网络编程离不开socket,接触不多时总觉得简单,无非是bind();listen();accept();send();recv()几个函数痴痴用上,能跑就不管三七二十一了。最近需要写个http代理,遇上几个问题。
socket的本质
从前认死理,怎么就recv()收到网络报文,功能还这么强大,嗅探,代理,服务器都用上它,就差没往硬件上想,现在想来也算是和自己和解了,在我的理解,socket是从传输层提取出的一套程序接口,供应用层使用,使我们不必面对实际的tcp/ip复杂的传输过程。它是一套规则和机制,灵感来源于Unix一切皆文件的设计,socket就像一个文件夹,创建,读取,写出,关闭,只不过这里对应的是网络数据。
socket中的accept()
accept()函数返回值是客户端socket和客户端地址信息,它创建了一个新的socket,称它为cli_socket,而原先绑定的socket称为soc。这里使我疑惑的地方就在于为什么同一个端口下有两个socket的存在。不是一个端口标识唯一的socket吗?
事实上,不是一个端口唯一标识,而是五元组标识一个socket,(source ip, source port, destination ip, destination port, protocol),而本地绑定的socket并不算一个完整的socket,它只包含三元组,即(destination ip, destination port, protocol),而accept()函数接受了来自客户端的信息,至此完成一个完整的socket链接,这个cli_socket用于接收和发送数据,而soc则继续用于监听客户端的connect请求。
再说明一点,虽说五元组唯一标识一个socket,并不是说这个端口就只能给一个socket使用,若是五元组中有不同的元素,比如说客户端ip和端口不同,那就是另一个socket,它和服务器端相同的端口和ip构成了新的五元组,比如说服务器端的80端口供多个客户端使用。
总的来说这些都是前人留下的规定,有的也不见得多明智,但目前还是主流,所以还是有必要知道。
参考1
参考2
socket中的数据接收问题
因为写的是一个代理,所以既要当客户端也要当服务端,在充当客户端过程中,需要和web服务器通信,很简单的一个建立连接,接收数据,是这样的:
SerSock = socket(AF_INET, SOCK_STREAM)
SerSock.connect(SerAddr)
SerSock.send(CMessage)
message = SerSock.recv(4096)
SerSock.close()
就是创建,连接,发送,接收,关闭的过程,但是报错,broken pipe。说是服务端还没传完数据,客户端就关闭了连接。
将接收到的数据输出了一下,发现数据都没传完,是这种:
细想,才知服务器传送数据并不是一次性传完的,学过计算机网络的都知道,传输层会将数据进行分片传输,选择不同的路径传送过来再组织到一起。那么分片的大小是多大呢?每个以太网帧都有最小的大小64bytes最大不能超过1518bytes,参考。
将代码一改:
SerSock = socket(AF_INET, SOCK_STREAM)
SerSock.connect(SerAddr)
SerSock.send(CMessage)
FromSMessage = ''
while True:
message = SerSock.recv(4096)
if not message:
break
FromSMessage += message
SerSock.close()
可顺利运行。
socket中的非阻塞问题
代理程序虽然能运行,但访问一多就会崩掉。这里需要知道的是,socket中的send(),recv()函数都是阻塞函数,也就是说若数据没有发送完毕或者没有接收完毕,函数是不会返回的,那么随着accept的客户端请求多了,内存占用溢出,就会崩掉。怎样设计一个非阻塞的socket程序呢?
这里主要讲一下select这个对象,利用select对象的select()函数可以实现非阻塞,简单看一下:
select.select(rlist, wlist, xlist[, timeout])
select()的参数为3个列表:第一列表为读取输入数据的对象;第2个接收要发送的数据,第3个存放errors,加上一个超时设置。
返回值有三个:readable,writable,exceptional
readable有3种可能:对于用来侦听连接主服务器socket,表示已准备好接受一个到来的连接;对于已经建立并发送数据的链接,表示有数据到来;如果没数据到来,表示链接已经关闭。
writable的情况:连接队列中有数据,发送下一条消息。如果队列中无数据,则从output队列中删除。
socket有错误,也要从output队列中删除。
所以这里实现非阻塞大体流程就是,select函数轮循,看有那个socket队列有需要读或写的数据,有就全部接收或发送,没有就忙别的。代码大体如下:
def nonblocking(self):
inputs=[self.client,self.target]
while True:
readable,writeable,errs=select.select(inputs,[],inputs,3)
if errs:
break
for soc in readable:
data=soc.recv(self.BUFSIZE)
if data:
if soc is self.client:
self.target.send(data)
elif soc is self.target:
self.client.send(data)
else:
break
self.client.close()
self.target.close()
至此,socket问题肯定不止于此,经事尚少,暂且留意了这些,做个总结,也希望能对他人有所帮助,不对之处还望指正。