socket(套接字)是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,将复杂的TCP/IP协议族隐藏在接口后面,让socket去组织数据以符合指定的协议。
如下左图为socket在tcp/ip协议中的角色,右图为socket的工作流程。
套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族:AF_UNIX
unix一切皆文件,基于文件的套接字调用底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族:AF_INET还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个。python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候只使用AF_INET
#server端 from socket import * phone = socket(AF_INET,SOCK_STREAM) #创建socket,第一个参数指定socket家族,第二个指定类型,SOCK_STREAM为tcp,SOCK_DGRAM为UDP phone.bind(('127.0.0.1',8000)) #socket绑定ip和端口,ip应该是本机地址 phone.listen(5) #socket开启监听,此时触发三次握手,参数表示可以挂起的请求个数 while True: conn,addr = phone.accept() #接收客户端连接,阻塞直至客户端发送消息 while True: try: msg = conn.recv(1024) #接收客户端消息 print('收到客户端的消息:',msg) conn.send(msg.upper()) #向客户端发送消息 except Exception: break conn.close() #关闭连接 phone.close() #关闭socket
#client端 from socket import * phone = socket(AF_INET,SOCK_STREAM) #创建客户端socket phone.connect(('127.0.0.1',8000)) #socket连接服务端,ip为服务端地址 while True: msg = input('请输入').strip() phone.send(msg.encode('utf-8')) #向服务端发送消息 msg = phone.recv(1025) #接收服务端消息 print('收到服务端的消息',msg) phone.close() #关闭客户端socket,触发四次挥手
server端流程:创建socket→绑定ip和端口→开启监听→接收连接→收/发消息→关闭连接→关闭socket
client端流程:创建socket→连接服务端→收/发消息→关闭连接
由于tcp是基于连接的,因此必须先启动服务端,然后再启动客户端去连接服务端。
由于socket是基于tcp/ip协议的,发送和接收消息必须是二进制数据,因此客户端需要通过encode('utf-8')去进行编码
对于服务端:
<socket.socket fd=224, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 58317)>
('127.0.0.1', 58317)
在linux系统中,如果服务端程序关闭后再马上启动,可能会报ip地址被占用,这是因为四次挥手需要时间。可以在服务端的bind操作前增加phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1).
#server端 from socket import * ip_port = ('127.0.0.1',8000) buffer_size = 1024 udp_server = socket(AF_INET,SOCK_DGRAM) udp_server.bind(ip_port) while True: msg,addr = udp_server.recvfrom(buffer_size) print(msg) udp_server.sendto(msg.upper(),addr)
#client端 from socket import * ip_port = ('127.0.0.1',8000) buffer_size = 1024 udp_client = socket(AF_INET,SOCK_DGRAM) while True: msg = input('请输入-->').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) msg,addr = udp_client.recvfrom(buffer_size) print(msg)
server端流程:创建socket→绑定ip和端口→收/发消息
client端流程:创建socket→收/发消息(发消息需指定服务端ip和端口)
对于UDP的socket,由于无连接因此无需进行监听。
基于UDP的发送和接收数据,接收需要使用recvfrom(),发送需要使用sendto('二进制数据',对方ip和端口)
tcp的socket的recv()得到的数据就是发送的字符串,udp的socket的recvfrom()得到的数据是一个元组,元组中第一个值为发送的字符串,第二个值为发送端ip和端号。
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务,收发两端(客户端和服务器端)都要有一一成对的socket。发送端为了将多个包更有效地发往接收端,使用了优化方法(Nagle算法)将多次间隔较小且数据量较小的数据合并成一个大的数据块,然后进行封包;这样接收端就难于分辨出来数据块中的界限,必须提供科学的拆包机制, 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务,不会使用块的合并优化算法。由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样对于接收端来说就容易进行区分处理了,即面向消息的通信是有消息保护边界的。
总结:tcp是基于数据流的,收发消息不能为空,需要在客户端和服务端都添加空消息的处理机制防止程序卡住;而udp是基于数据报的,即便输入的是空内容(直接回车),实际也不是空消息,udp协议会封装上消息头。
粘包只发生在tcp协议中。由于tcp协议数据不会丢,如果一次没有接收完数据包,那么下次接收会从上次接收完的地方继续接收,并且己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包,粘包的发生有以下两种情况。
执行结果如下,可见在基于tcp的socket中,一次recv并不对应一次send,send是向自身缓冲区发送数据,recv也是从自身缓冲区获取数据,recv和send没有对应关系。
而udp协议中的recvfrom和sendto是一一对应的关系,如果超出缓冲区大小接收方直接丢弃。
执行结果如下,可见第二次和第三次都是在上一次接收的地方继续接收数据的。
以上发生粘包的两种情况,本质都是接收端不知道发送端发送数据的大小,导致接收时获取的数据大小与发送的不一致。因此可以在发送端发送数据时,同时将数据大小也发送过去,接收端根据这个大小去获取发送的数据。
发送数据大小的实现方法:发送端先计算出数据的大小,将这个整型数字通过struct.pack('i',l)打包成4个字节的二进制,然后发送打包后的这4个字节,再发送实际数据。在实际发送时这两部分会发生粘包一起发送。接收端先获取4个字节的,再通过struct.unpack('i',l)解包拿到实际数据的大小。
由于udp无连接故可实现并发,而上面几个关于tcp的socket的例子无法实现并发,即服务端如果已经接受一个连接,其他的连接无法进入,必须在当前连接中断后才可重新建立连接。通过socketserver可实现tcp的并发。socketserver需要自定义一个继承socketserver.BaseRequestHandler的类,并在类中定义一个handle方法;通过socketserver建立多线程或多进程的连接,并通过serve_forever实现多连接。
将上述tcp_client复制多份,可以发现tcp_server可同时接收多个client的请求并成功返回。
将上述udp_client复制多份,udp_server也可同时接收多个client的请求并成功返回。
对于tcp中自定义的类,self.request表示连接(即相当于accept()返回的conn),需要再调用recv()去接收数据
对于udp中自定义的类,self.request为一个元组,元组中的第一个元素为接收的数据,第二个元素为socket对象,即self.request[0]为接收数据,通过self.request[0].sendto('xxx',self.client_address)去发送数据
两者的self.client_address都表示客户端的ip和端口
联系客服