非阻塞Python套接字
我想为我的诺基亚手机写一个小的蓝牙服务器应用,使用的是PyS60。这个应用需要能够对客户端的请求做出回应,并且能够主动向客户端发送数据。
选项 1:
如果我使用 socket.recv(1024)
,程序会一直等待接收到数据,这样服务器就无法主动向客户端发送数据了。而且,Python for S60的实现中没有 socket.settimeout()
这个方法,所以我无法写出合适的非阻塞代码。
选项 2:
我尝试了 socket.makefile()
的方法,看起来不错,但没能成功。当我把 conn.recv(1024)
替换成 fd = socket.makefile() fd.readline()
时,它什么都没读到。
选项 3:
我查看了 select()
函数,但也没有成功。当我把 conn.recv()
改成 r,w,e = select.select([conn],[],[])
,像建议的那样,客户端甚至都无法连接。它一直停在“等待客户端...”的状态。真奇怪……
我知道有一些很不错的服务器实现和异步API,但我这里只需要一些非常基础的功能。提前谢谢大家!
这是我目前的代码:
sock = btsocket.socket(btsocket.AF_BT, btsocket.SOCK_STREAM)
channel = btsocket.bt_rfcomm_get_available_server_channel(sock)
sock.bind(("", channel))
sock.listen(1)
btsocket.bt_advertise_service(u"name", sock, True, btsocket.RFCOMM)
print "Waiting for the client..."
conn, client_mac = sock.accept()
print "connected: " + client_mac
while True:
try:
data = conn.recv(1024)
if len(data) != 0:
print "received [%s]" % data
if data.startswith("something"): conn.send("something\r\n")
else:
conn.send("some other data \r\n")
except:
pass
显然这是阻塞的,所以“其他一些数据”永远不会被发送,但这是我目前为止得到的最好结果。至少我可以对客户端发送一些回复。
3 个回答
这里有一个Epoll服务器的实现(非阻塞式)
http://pastebin.com/vP6KPTwH (和下面的内容一样,觉得这个链接可能更方便复制)
使用 python epollserver.py
来启动服务器。
可以用 wget localhost:8888
来测试它
import sys import socket, select import fcntl import email.parser import StringIO import datetime """ See: http://docs.python.org/library/socket.html """ __author__ = ['Caleb Burns', 'Ben DeMott'] def main(argv=None): EOL1 = '\n\n' EOL2 = '\n\r\n' response = 'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n' response += 'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n' response += 'Hello, world!' serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Tell the server socket file descriptor to destroy itself when this program ends. socketFlags = fcntl.fcntl(serversocket.fileno(), fcntl.F_GETFD) socketFlags |= fcntl.FD_CLOEXEC fcntl.fcntl(serversocket.fileno(), fcntl.F_SETFD, socketFlags) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.bind(('0.0.0.0', 8888)) serversocket.listen(1) # Use asynchronous sockets. serversocket.setblocking(0) # Allow a queue of up to 128 requests (connections). serversocket.listen(128) # Listen to socket events on the server socket defined by the above bind() call. epoll = select.epoll() epoll.register(serversocket.fileno(), select.EPOLLIN) print "Epoll Server Started..." try: #The connection dictionary maps file descriptors (integers) to their corresponding network connection objects. connections = {} requests = {} responses = {} while True: # Ask epoll if any sockets have events and wait up to 1 second if no events are present. events = epoll.poll(1) # fileno is a file desctiptor. # event is the event code (type). for fileno, event in events: # Check for a read event on the socket because a new connection may be present. if fileno == serversocket.fileno(): # connection is a new socket object. # address is client IP address. The format of address depends on the address family of the socket (i.e., AF_INET). connection, address = serversocket.accept() # Set new socket-connection to non-blocking mode. connection.setblocking(0) # Listen for read events on the new socket-connection. epoll.register(connection.fileno(), select.EPOLLIN) connections[connection.fileno()] = connection requests[connection.fileno()] = b'' responses[connection.fileno()] = response # If a read event occured, then read the new data sent from the client. elif event & select.EPOLLIN: requests[fileno] += connections[fileno].recv(1024) # Once we're done reading, stop listening for read events and start listening for EPOLLOUT events (this will tell us when we can start sending data back to the client). if EOL1 in requests[fileno] or EOL2 in requests[fileno]: epoll.modify(fileno, select.EPOLLOUT) # Print request data to the console. epoll.modify(fileno, select.EPOLLOUT) data = requests[fileno] eol = data.find("\r\n") #this is the end of the FIRST line start_line = data[:eol] #get the contents of the first line (which is the protocol information) # method is POST|GET, etc method, uri, http_version = start_line.split(" ") # re-used facebooks httputil library (works well to normalize and parse headers) headers = HTTPHeaders.parse(data[eol:]) print "\nCLIENT: FD:%s %s: '%s' %s" % (fileno, method, uri, datetime.datetime.now()) # If the client is ready to receive data, sent it out response. elif event & select.EPOLLOUT: # Send response a single bit at a time until the complete response is sent. # NOTE: This is where we are going to use sendfile(). byteswritten = connections[fileno].send(responses[fileno]) responses[fileno] = responses[fileno][byteswritten:] if len(responses[fileno]) == 0: # Tell the socket we are no longer interested in read/write events. epoll.modify(fileno, 0) # Tell the client we are done sending data and it can close the connection. (good form) connections[fileno].shutdown(socket.SHUT_RDWR) # EPOLLHUP (hang-up) events mean the client has disconnected so clean-up/close the socket. elif event & select.EPOLLHUP: epoll.unregister(fileno) connections[fileno].close() del connections[fileno] finally: # Close remaining open socket upon program completion. epoll.unregister(serversocket.fileno()) epoll.close() serversocket.close() #!/usr/bin/env python # # Copyright 2009 Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """HTTP utility code shared by clients and servers.""" class HTTPHeaders(dict): """A dictionary that maintains Http-Header-Case for all keys. Supports multiple values per key via a pair of new methods, add() and get_list(). The regular dictionary interface returns a single value per key, with multiple values joined by a comma. >>> h = HTTPHeaders({"content-type": "text/html"}) >>> h.keys() ['Content-Type'] >>> h["Content-Type"] 'text/html' >>> h.add("Set-Cookie", "A=B") >>> h.add("Set-Cookie", "C=D") >>> h["set-cookie"] 'A=B,C=D' >>> h.get_list("set-cookie") ['A=B', 'C=D'] >>> for (k,v) in sorted(h.get_all()): ... print '%s: %s' % (k,v) ... Content-Type: text/html Set-Cookie: A=B Set-Cookie: C=D """ def __init__(self, *args, **kwargs): # Don't pass args or kwargs to dict.__init__, as it will bypass # our __setitem__ dict.__init__(self) self._as_list = {} self.update(*args, **kwargs) # new public methods def add(self, name, value): """Adds a new value for the given key.""" norm_name = HTTPHeaders._normalize_name(name) if norm_name in self: # bypass our override of __setitem__ since it modifies _as_list dict.__setitem__(self, norm_name, self[norm_name] + ',' + value) self._as_list[norm_name].append(value) else: self[norm_name] = value def get_list(self, name): """Returns all values for the given header as a list.""" norm_name = HTTPHeaders._normalize_name(name) return self._as_list.get(norm_name, []) def get_all(self): """Returns an iterable of all (name, value) pairs. If a header has multiple values, multiple pairs will be returned with the same name. """ for name, list in self._as_list.iteritems(): for value in list: yield (name, value) def items(self): return [{key: value[0]} for key, value in self._as_list.iteritems()] def get_content_type(self): return dict.get(self, HTTPHeaders._normalize_name('content-type'), None) def parse_line(self, line): """Updates the dictionary with a single header line. >>> h = HTTPHeaders() >>> h.parse_line("Content-Type: text/html") >>> h.get('content-type') 'text/html' """ name, value = line.split(":", 1) self.add(name, value.strip()) @classmethod def parse(cls, headers): """Returns a dictionary from HTTP header text. >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n") >>> sorted(h.iteritems()) [('Content-Length', '42'), ('Content-Type', 'text/html')] """ h = cls() for line in headers.splitlines(): if line: h.parse_line(line) return h # dict implementation overrides def __setitem__(self, name, value): norm_name = HTTPHeaders._normalize_name(name) dict.__setitem__(self, norm_name, value) self._as_list[norm_name] = [value] def __getitem__(self, name): return dict.__getitem__(self, HTTPHeaders._normalize_name(name)) def __delitem__(self, name): norm_name = HTTPHeaders._normalize_name(name) dict.__delitem__(self, norm_name) del self._as_list[norm_name] def get(self, name, default=None): return dict.get(self, HTTPHeaders._normalize_name(name), default) def update(self, *args, **kwargs): # dict.update bypasses our __setitem__ for k, v in dict(*args, **kwargs).iteritems(): self[k] = v @staticmethod def _normalize_name(name): """Converts a name to Http-Header-Case. >>> HTTPHeaders._normalize_name("coNtent-TYPE") 'Content-Type' """ return "-".join([w.capitalize() for w in name.split("-")]) if(__name__ == '__main__'): sys.exit(main(sys.argv))
这里有一个简单的例子,基于一个回声服务器。
#!/usr/bin/python
import socket
import select
server = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
server.bind( ('localhost', 12556) )
server.listen( 5 )
toread = [server]
running = 1
# we will shut down when all clients disconenct
while running:
rready,wready,err = select.select( toread, [], [] )
for s in rready:
if s == server:
# accepting the socket, which the OS passes off to another
# socket so we can go back to selecting. We'll append this
# new socket to the read list we select on next pass
client, address = server.accept()
toread.append( client ) # select on this socket next time
else:
# Not the server's socket, so we'll read
data = s.recv( 1024 )
if data:
print "Received %s" % ( data )
else:
print "Client disconnected"
s.close()
# remove socket so we don't watch an invalid
# descriptor, decrement client count
toread.remove( s )
running = len(toread) - 1
# clean up
server.close()
不过,我觉得socketserver这个库更简洁、更容易使用。只需要实现处理请求的函数,然后调用serve_forever就可以了。
终于找到解决办法了!
原来,select函数在新版本的PyS60的btsocket模块中不太好用。有人写了一个新的btsocket(可以在这里找到),这个新版本的select函数可以正常工作。