使用Python创建WebSocket客户端

7

我正在尝试学习套接字编程以及WebSocket协议。我知道已经存在Python WebSocket客户端,但我希望为自己的学习构建一个玩具版本。为此,我创建了一个非常简单的Tornado WebSocket服务器,我正在localhost:8888上运行它。它的全部作用就是在客户端连接时打印一条消息。

这就是整个服务器 - 它可以工作(我已经用浏览器中的小javascript脚本进行了测试)。

import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web


class WSHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        print('new connection')
        self.write_message("Hello World")

    def on_message(self, message):
        print('message received %s' % message)

    def on_close(self):
      print('connection closed')

application = tornado.web.Application([
    (r'/ws', WSHandler),
])


if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

一旦我启动服务器,我就尝试运行以下脚本:

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((socket.gethostbyname('localhost'), 8888))

msg = '''GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13'''.encode('ascii')
print(len(msg))

sent_count = sock.send(msg)
print('sent this many bytes:', sent_count)
recv_value = sock.recv(1)
print('recvieved:', recv_value)

我希望服务器按照RFC规定发送响应头,但sock.recv却一直等待。这让我认为服务器没有确认websocket的初始握手。这个握手也是根据RFC进行的。我知道websocket key应该是随机的,但我不认为这会导致服务器忽略握手(websocket key有效)。我想一旦我能够发起握手,就能解决其余的问题,所以我希望只是对websockets的工作方式或如何发送初始握手存在一些误解。
1个回答

12

1) 当你通过套接字发送消息时,你不知道它会被分成多少块。它可能一次性全部发送;或者前三个字母被发送,然后是剩余的消息;或者该消息被分成10个部分。

2) 鉴于问题1),服务器应该如何知道它已经接收到客户端发送的所有块?例如,假设服务器接收到客户端消息的一个块。服务器如何知道那是整个消息还是还有9个块要来?

3) 我建议您阅读这篇文章:

http://docs.python.org/2/howto/sockets.html

(再加上评论中的链接)

4) 现在,为什么不使用Python创建HTTP服务器呢?

python3:

import http.server
import socketserver

PORT = 8000
handler = http.server.SimpleHTTPRequestHandler

httpd = socketserver.TCPServer(("", PORT), handler)

print("serving at port", PORT)
httpd.serve_forever()

Python2:

import SimpleHTTPServer
import SocketServer

PORT = 8000
handler = SimpleHTTPServer.SimpleHTTPRequestHandler

httpd = SocketServer.TCPServer(("", PORT), handler)

print "serving at port", PORT
httpd.serve_forever()

SimpleHTTPRequestHandler可以在服务器程序的目录及其子目录中提供文件,匹配您创建的目录结构和请求的URL。如果您请求'/',则服务器将从与服务器位于同一目录中的index.html文件提供服务。这是Python 3的客户端套接字示例(Python 2示例如下):

import socket   
import sys

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error:
    print('Failed to create socket')
    sys.exit()

print('Socket Created')

#To allow you to immediately reuse the same port after 
#killing your server:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

host = 'localhost';
port = 8000;

s.connect((host , port))

print('Socket Connected to ' + host + ' on port ', port)


#Send some data to server
message = "GET / HTTP/1.1\r\n\r\n"

try :
    #Send the whole string(sendall() handles the looping for you)
    s.sendall(message.encode('utf8') )
except socket.error:
    print('Send failed')
    sys.exit()

print('Message sent successfully')

#Now receive data
data = [] 

while True:
    chunk = s.recv(4096)  #blocks while waiting for data
    if chunk: data.append(chunk.decode("utf8"))
    #If the recv() returns a blank string, then the other side
    #closed the socket, and no more data will be sent:
    else: break  

print("".join(data))

--output:--
Socket Created
Socket Connected to localhost on port  8000
Message sent successfully
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.2.3
Date: Sat, 08 Jun 2013 09:15:18 GMT
Content-type: text/html
Content-Length: 23
Last-Modified: Sat, 08 Jun 2013 08:29:01 GMT

<div>hello world</div>
在Python 3中,你必须使用字节串来处理套接字,否则你将会遭受可怕的错误信息:
TypeError: 'str' does not support the buffer interface

这是 Python 2.x 的代码:

import socket   
import sys

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error:
    print 'Failed to create socket'
    sys.exit()

print('Socket Created')

#To allow you to immediately reuse the same port after 
#killing your server:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

host = 'localhost';
port = 8000;

s.connect((host , port))

print('Socket Connected to ' + host + ' on port ', port)

#Send some data to server
message = "GET / HTTP/1.1\r\n\r\n"

try :
    #Send the whole string(handles the looping for you)
    s.sendall(message)
except socket.error:
    print 'Send failed'
    sys.exit()

print 'Message sent successfully'

#Now receive data
data = [] 

while True:
    chunk = s.recv(4096)  #blocks while waiting for data
    if chunk: data.append(chunk)
    #If recv() returns a blank string, then the other side
    #closed the socket, and no more data will be sent:
    else: break  

print("".join(data))

--output:--
Message sent successfully
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/2.7.3
Date: Sat, 08 Jun 2013 10:06:04 GMT
Content-type: text/html
Content-Length: 23
Last-Modified: Sat, 08 Jun 2013 08:29:01 GMT

<div>hello world</div>

请注意,GET请求的头部会告诉服务器使用HTTP 1.1协议进行通信,即控制对话的规则。根据HTTP 1.1的RFC描述,请求中必须有两个'\r\n'序列。因此,服务器会寻找第二个'\r\n'序列。如果您从请求中删除一个'\r\n'序列,则客户端将在recv()上挂起,因为服务器仍在等待更多数据,因为服务器尚未读取第二个'\r\n'序列。

还要注意,在Python 3中,您将以字节形式发送数据,因此不会进行任何自动的'\n'转换,而服务器将期望序列'\r\n'。


有没有控制字节可以发送告诉服务器已经接收到整个头部?像0x00这样的东西?刚看到你的编辑,谢谢链接,我会阅读它。 - Bear
好的,现在你有头绪了。服务器是否期望收到0x00字节来表示消息的结束?还是服务器期望看到“END OF MESSAGE”?或者是一个换行符?每个这样的规则被称为协议,你必须事先达成协议,以便套接字和服务器知道如何相互通信。请阅读我发布的链接。 - 7stud
是的,在你分享链接之前我已经发布了那个帖子。但是你说的有道理。我需要让服务器知道我的消息已经发送完毕,这样它就可以进行处理/响应了。我正在阅读HOWTO的过程中,希望能让我找到正确的方法。 - Bear
@Bear,我不得不对Python3进行一些调整,现在有Python3和Python2的示例供您查看。 - 7stud
那真的很有用。我今晚会看一下。抱歉回复晚了,我整天都在工作。这一切让我走上了正确的轨道,这将产生巨大的影响。 - Bear
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接