一、socket简介
socket(套接字)是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,将复杂的TCP/IP协议族隐藏在接口后面,让socket去组织数据以符合指定的协议。
如下左图为socket在tcp/ip协议中的角色,右图为socket的工作流程。
二、socket分类
套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族:AF_UNIX
unix一切皆文件,基于文件的套接字调用底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族:AF_INET还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个。python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候只使用AF_INET
三、基于TCP的socket
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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,触发四次挥手
1.基于TCP的socket的工作流程
server端流程:创建socket→绑定ip和端口→开启监听→接收连接→收/发消息→关闭连接→关闭socket
client端流程:创建socket→连接服务端→收/发消息→关闭连接
2.关于TCP的socket的一些解释说明
由于tcp是基于连接的,因此必须先启动服务端,然后再启动客户端去连接服务端。
由于socket是基于tcp/ip协议的,发送和接收消息必须是二进制数据,因此客户端需要通过encode('utf-8')去进行编码
对于服务端:
- accept的返回值为两部分,第一部分为一个连接,第二部分为客户端的ip和端口,值如下
<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)
- 外层的while True循环是为了能够接受多个客户端的请求,否则只能建立一个连接
- 内层的while True循环是为了能够与同一个客户端进行多次收发消息,否则只能接收和发送一次消息
- 内层循环中的try···except异常处理,是为了防止一个客户端异常终止连接后conn失效导致服务端程序崩溃
在linux系统中,如果服务端程序关闭后再马上启动,可能会报ip地址被占用,这是因为四次挥手需要时间。可以在服务端的bind操作前增加phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1).
四、基于UDP的socket
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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)
1.基于UDP的socket的工作流程
server端流程:创建socket→绑定ip和端口→收/发消息
client端流程:创建socket→收/发消息(发消息需指定服务端ip和端口)
2.关于UDP的socket的一些解释说明
对于UDP的socket,由于无连接因此无需进行监听。
基于UDP的发送和接收数据,接收需要使用recvfrom(),发送需要使用sendto('二进制数据',对方ip和端口)
tcp的socket的recv()得到的数据就是发送的字符串,udp的socket的recvfrom()得到的数据是一个元组,元组中第一个值为发送的字符串,第二个值为发送端ip和端号。
五、socket的粘包现象
1.tcp和udp协议发送数据的过程
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务,收发两端(客户端和服务器端)都要有一一成对的socket。发送端为了将多个包更有效地发往接收端,使用了优化方法(Nagle算法)将多次间隔较小且数据量较小的数据合并成一个大的数据块,然后进行封包;这样接收端就难于分辨出来数据块中的界限,必须提供科学的拆包机制, 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务,不会使用块的合并优化算法。由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样对于接收端来说就容易进行区分处理了,即面向消息的通信是有消息保护边界的。
总结:tcp是基于数据流的,收发消息不能为空,需要在客户端和服务端都添加空消息的处理机制防止程序卡住;而udp是基于数据报的,即便输入的是空内容(直接回车),实际也不是空消息,udp协议会封装上消息头。
2.粘包
粘包只发生在tcp协议中。由于tcp协议数据不会丢,如果一次没有接收完数据包,那么下次接收会从上次接收完的地方继续接收,并且己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包,粘包的发生有以下两种情况。
①发送端在短时间内多次发送较小数据,实际会按照优化算法合并发送
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from socket import * ip_port = ('127.0.0.1',8002) buffer_size = 1024 back_log = 5 tcp_server = socket(AF_INET,SOCK_STREAM) tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_server.bind(ip_port) tcp_server.listen(back_log) while True: conn,addr = tcp_server.accept() data1 = conn.recv(buffer_size) print('第一次接收数据',data1) data2 = conn.recv(buffer_size) print('第一次接收数据', data2) data3 = conn.recv(buffer_size) print('第一次接收数据', data3) conn.close() tcp_server.close()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from socket import * ip_port = ('127.0.0.1',8002) tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_client.connect(ip_port) tcp_client.send('hello'.encode('utf-8')) tcp_client.send('world'.encode('utf-8')) tcp_client.send('happy'.encode('utf-8')) tcp_client.send('python'.encode('utf-8')) tcp_client.close()
执行结果如下,可见在基于tcp的socket中,一次recv并不对应一次send,send是向自身缓冲区发送数据,recv也是从自身缓冲区获取数据,recv和send没有对应关系。
而udp协议中的recvfrom和sendto是一一对应的关系,如果超出缓冲区大小接收方直接丢弃。
②接收端一次接收的数据小于发送数据,下次接收时会从上次接收完的地方继续接收
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from socket import * ip_port = ('127.0.0.1',8002) back_log = 5 tcp_server = socket(AF_INET,SOCK_STREAM) tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_server.bind(ip_port) tcp_server.listen(back_log) while True: conn,addr = tcp_server.accept() data1 = conn.recv(2) print('第一次接收数据',data1) data2 = conn.recv(5) print('第一次接收数据', data2) data3 = conn.recv(6) print('第一次接收数据', data3) conn.close() tcp_server.close()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from socket import * ip_port = ('127.0.0.1',8002) tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_client.connect(ip_port) tcp_client.send('helloworldhappypython'.encode('utf-8')) tcp_client.close()
执行结果如下,可见第二次和第三次都是在上一次接收的地方继续接收数据的。
3.解决粘包
以上发生粘包的两种情况,本质都是接收端不知道发送端发送数据的大小,导致接收时获取的数据大小与发送的不一致。因此可以在发送端发送数据时,同时将数据大小也发送过去,接收端根据这个大小去获取发送的数据。
发送数据大小的实现方法:发送端先计算出数据的大小,将这个整型数字通过struct.pack('i',l)打包成4个字节的二进制,然后发送打包后的这4个字节,再发送实际数据。在实际发送时这两部分会发生粘包一起发送。接收端先获取4个字节的,再通过struct.unpack('i',l)解包拿到实际数据的大小。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from socket import * import subprocess,struct ip_port = ('127.0.0.1',8001) buffer_size = 1024 back_log = 5 tcp_server = socket(AF_INET,SOCK_STREAM) tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_server.bind(ip_port) tcp_server.listen(back_log) while True: conn,addr = tcp_server.accept() print('新的客户端连接进入',addr) while True: try: cmd = conn.recv(buffer_size) if not cmd:break #如果发送的命令为空,终止当前循环等待下一个连接进入 print('收到客户端的命令为',cmd.decode('utf-8')) res = subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout = subprocess.PIPE,stdin = subprocess.PIPE,stderr = subprocess.PIPE) err = res.stderr.read() if err: cmd_res = err #如果err有内容,表示命令输入错误,执行结果就为err else: cmd_res = res.stdout.read() #如果err无内容,表示命令执行成功,执行结果为标准输出的内容 if not cmd_res: #如果命令执行成功但是没有输出,例如cd,返回一个执行成功 cmd_res = '执行成功'.encode('gbk') cmd_res_length = len(cmd_res) #获取执行结果的长度 pack_cmd_res = struct.pack('i',cmd_res_length) #将长度打包成4个字节的二进制形式 conn.send(pack_cmd_res) conn.send(cmd_res) #4个字节会和实际发送数据粘包一起发送 except Exception as e: print(e) break
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from socket import * import struct ip_port = ('127.0.0.1',8001) buffer_size = 1024 back_log = 5 tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: cmd = input('请输入命令:').strip() if not cmd:continue if cmd == 'quit':break tcp_client.send(cmd.encode('utf-8')) cmd_res_length = struct.unpack('i',tcp_client.recv(4))[0] #先接受4个字节并unpack,获取执行结果的长度 cmd_res = b'' tmp_length = 0 while tmp_length < cmd_res_length: #如果临时长度小于执行结果的长度,则循环接受buffer_size大小的数据 cmd_res += tcp_client.recv(buffer_size) tmp_length = len(cmd_res) print('命令执行结果为',cmd_res.decode('gbk')) tcp_client.close()
六、tcp实现并发
1.socket实现tcp并发
由于udp无连接故可实现并发,而上面几个关于tcp的socket的例子无法实现并发,即服务端如果已经接受一个连接,其他的连接无法进入,必须在当前连接中断后才可重新建立连接。通过socketserver可实现tcp的并发。socketserver需要自定义一个继承socketserver.BaseRequestHandler的类,并在类中定义一个handle方法;通过socketserver建立多线程或多进程的连接,并通过serve_forever实现多连接。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): #必须自定义一个handle()方法 print('conn:',self.request) #相当于accept()返回的conn print('addr:',self.client_address) #相当于accept()返回的addr while True: try: data = self.request.recv(1024) print('接收到的客户端消息:',data) self.request.sendall(data.upper()) except Exception as e: print(e) break if __name__ == '__main__': # 实例化一个对象,建立连接 s = socketserver.ThreadingTCPServer(('127.0.0.1',8005),MyServer) # 多线程 # s = socketserver.ForkingTCPServer(('127.0.0.1',8005),MyServer) 多进程,系统开销多余多线程,常用ThreadingTCPServer print(s.server_address) #相当于实例化传入的第一个参数 print(s.RequestHandlerClass) #实例化传入的第二个参数 print(MyServer) print(s.socket) #socket对象 s.serve_forever()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from socket import * tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.connect(('127.0.0.1',8005)) while True: msg = input('请输入:').strip() if not msg : continue elif msg == 'quit': break else: tcp_client.send(msg.encode('utf-8')) data = tcp_client.recv(1024) print('接收到的服务端消息:',data.decode('utf-8')) tcp_client.close()
将上述tcp_client复制多份,可以发现tcp_server可同时接收多个client的请求并成功返回。
2.socket实现udp并发
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): print(self.request) try: data = self.request[0] print('接收到的客户端消息:', data) self.request[1].sendto(data.upper(),self.client_address) except Exception as e: print(e) if __name__ == '__main__': s = socketserver.ThreadingUDPServer(('127.0.0.1',8006),MyServer) s.serve_forever()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from socket import * udp_client = socket(AF_INET,SOCK_DGRAM) udp_client.connect(('127.0.0.1',8006)) while True: msg = input('请输入:').strip() if not msg : continue elif msg == 'quit': break else: udp_client.sendto(msg.encode('utf-8'),('127.0.0.1',8006)) data,addr = udp_client.recvfrom(1024) print('接收到的服务端消息:',data.decode('utf-8')) udp_client.close()
将上述udp_client复制多份,udp_server也可同时接收多个client的请求并成功返回。
3.socketserver对于tcp和udp的区别
对于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和端口