SOCKET : socket本质上来说就是位于应用层与传输层之间的逻辑层,下图为tcp/ip四层,五层协议以及osi参考模型
一般现在用的就是五层模型,从两台计算机传输数据的角度来讲,物理层定义其计算机基本数据结构(0和1,也就是数据本身)。数据链路层定义了数据的传输(以mac地址为唯一标识),而这一层在局域网内通信方式也极其简单,就是通过广播发送到每一个局域网内的计算机上面,如非指定的接受者(mac地址),则丢弃。到网络层时就可以跨越其局域网的限制,通过网关设备的路由功能(通过ip地址进行路由),就可以找到互联网上的每一个设备进行传输通信,但其本质上还是数据链路层通信,网络层最主要是定位到互联网每一台主机(ARP协议最终将ip解析到mac地址)。到传输层时有两个协议(tcp和udp),tcp是稳定的传输协议,主要保证数据完整性,而udp是非稳定的传输协议,用于非重要数据传输,例如视频聊天,qq信息等。这里传输层定义了端口,可使用范围为1-65535。如果说网络层是用来定位到互联网中的某一台计算机的话,那么传输层则是定位到计算机中的某个应用且与其建立连接(三次握手,其建立连接本质上也是逻辑上的建立)。到应用层这里则是访问到应用中具体某个资源,例如/index.html。
应用程序位于其应用层,在应用程序发送数据时,需要将其封装打包成一段段的数据段送到传输层,然后传输层再封装成网络包,到链路层时又封装成数据帧,到物理层又封装成数据位(二进制,0和1)。到目标主机以后从物理层开始又一层一层的拆包往上传递,所以正常情况下程序员在编写程序时,应该把客户端和目标服务器五层的封装以及拆包写好,但是这个步骤太过于麻烦且重复性工作,所以有了socket。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
有了socket接口以后程序员就只用关心程序本身,只要将C/S两边程序写好,直接调用已有的socket模块来帮程序员完成应用层以下真正实现传输的所有步骤。
socket流程:
图中每一个步骤就是socket的每一个提供函数(方法)。
客户端:
首先创建socket对象,连接服务器端,发送(请求)数据和接受数据,然后关闭套接字
服务器端: 首先也需要创建socket对象,然后绑定端口(服务器需要固定的端口绑定,客户端使用随机ip和端口进行通信即可),监听,监听完毕后accept等待客户端连接(这里可以设置半连接池),然后连接完毕以后发送数据,接受数据(发送接受先后顺序不定,看程序需要),最后关闭连接(关闭这个客户端的连接),但一般不会关闭服务器套接字连接。
socket for python: tcp流程 tcp客户端: 创建客户端套接字对象 和服务端套接字建立连接 发送数据 接收数据 关闭客户端套接字 socket() -> connect() -> send() -> recv () ->close() tcp服务器: 创建服务端端套接字对象 绑定端口号 设置监听 等待接受客户端的连接请求 接收数据 发送数据 关闭套接字 socket()->bind()->listen()->accept()->recv()-send()->close()
tcp客户端开发:
需要导入socket模块,然后根据步骤调用每一个方法即可,实例代码如下
tcp: 客户端:
1 2 3 4 5 6 7 8 9 10 import socket SOCKET=socket.socket (socket.AF_INET ,socket.SOCK_STREAM) SOCKET.connect (('localhost' ,8080 )) SOCKET.send ('你就是传说中的服务器吗?我是客户端' .encode ('utf-8' )) information=SOCKET.recv (1024 )print (information.decode('utf-8' ) ) SOCKET.close ()
服务器端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import socket SOCKET=socket.socket (socket.AF_INET ,socket.SOCK_STREAM) SOCKET.bind (('localhost' ,8080 )) SOCKET.listen () user,user_addr=SOCKET.accept ()print ('正在处理客户端消息,客户端ip为:' ,user_addr) information=user.recv (1024 )print (information.decode('utf-8' ) ) user.send ('给客户端发送消息' .encode ('utf-8' )) user.close () SOCKET.close ()
udp: 服务端:
1 2 3 4 5 6 7 8 import socketserver =socket.socket(socket.AF_INET ,socket.SOCK_DGRAM )server .bind(('127.0 .0 .1 ',8080 ))while True : data ,ip_addr=server.recvfrom(1024) print(data .decode('utf -8')) server.sendto(data ,ip_addr)server .close()
客户端:
1 2 3 4 5 6 7 8 import socket server = socket.socket (socket.AF_INET , socket.SOCK_DGRAM) while True: msg=input ('>>>:' ).strip () server.sendto (msg.encode ('utf-8' ),('127.0.0.1' ,8080 )) data, ip_addr = server.recvfrom (1024 ) print(data.decode ('utf-8' )) server.close ()
远程执行命令和粘包问题 粘包:
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。 例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束 所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。 此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
1.TCP (transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端〈客户端和服务器端)都要有—一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。 2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头〈消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。即面向消息的通信是有消息保护边界的。 3.tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头.
例如客户端在使用recv(1024)方法接受数据时,其最大接受数据为1024个字节,但服务器端发送的数据远远大于这个值,于是客户端的缓存就会有很多数据没拿到,只有1024是真正被网卡取出缓存的,其他数据会在下一次再向客户端请求数据时拿到,第二次很明显无法拿到想要的数据,而是第一次没有读取完的数据。
客户端收数据没收干净,有残留,就会在下一次结果混淆在一起。
解决办法:
每次都将数据收完,不要让其残留在缓存里面。
1.拿到数据的总大小total_size
2.recv_size=0,循环接收,每接收一次,recv_size+=接收的长度
3.直到recv_size=total_size,结束循环
python解决粘包(远程执行命令的代码): 1 2 3 1. 服务器端要传入其发送数据的具体字节大小到客户端(也就是头信息)2. 客户端接收数据时要先接收头部信息(字节大小),再根据其字节大小接收具体的数据#subprocess为远程执行命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import subprocessfrom socket import * import struct server=socket (AF_INET,SOCK_STREAM) server.bind(('127.0.0.1' ,8080 )) server.listen(5 )while True: conn,client_addr=server.accept() while True: try : res=conn.recv(1024 ) if len (res) == 0 :break obj=subprocess.Popen(res.decode('utf-8' ), shell =True, stdout =subprocess.PIPE, stderr =subprocess.PIPE) stdout =obj.stdout .read () stderr =obj.stderr .read () total_size=len (stdout )+len (stderr ) header=struct.pack('i' ,total_size) conn.send (header) conn.send (stdout +stderr ) except Exception: break conn.close ()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import structfrom socket import *client =socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1' ,8080))while True : msg =input('请输入命令').strip() if len(msg) == 0:continue client.send(msg.encode('utf-8' )) header =client.recv(4) #拿四个字节的数据大小 total_size =struct.unpack('i',header)[0] #拿到后解码 recv_size =0 cmd_res =b'' while recv_size < total_size: #如果已拿到的数据大小recv_size小于服务器发送过来的总数据大小就继续拿数据。 data =client.recv(1024) recv_size+=len(data) cmd_res+=data print (data.decode('gbk' ),end ='' ) #windows编码gbk else : print ('' )
strut转换参数:
解决粘包代码终极版: (将其封装成一个字典,将字典作为头信息传递过去)
服务器端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 """ @作者: egon老湿 @微信:18611453110 @专栏: https://zhuanlan.zhihu.com/c_1189883314197168128 """ import subprocessimport structimport jsonfrom socket import * server=socket(AF_INET,SOCK_STREAM) server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1 ) server.bind(('127.0.0.1' ,8083 )) server.listen(5 )while True : conn,client_addr=server.accept() while True : try : cmd=conn.recv(1024 ) if len (cmd) == 0 :break obj=subprocess.Popen(cmd.decode('utf-8' ), shell=True , stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout_res=obj.stdout.read() stderr_res=obj.stderr.read() total_size=len (stdout_res)+len (stderr_res) header_dic={ "filename" :"a.txt" , "total_size" :total_size, "md5" :"123123xi12ix12" } json_str = json.dumps(header_dic) json_str_bytes = json_str.encode('utf-8' ) x=struct.pack('i' ,len (json_str_bytes)) conn.send(x) conn.send(json_str_bytes) conn.send(stdout_res) conn.send(stderr_res) except Exception: break conn.close()
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 "" " @作者: egon老湿 @微信:18611453110 @专栏: https://zhuanlan.zhihu.com/c_1189883314197168128 " "" import struct import jsonfrom socket import *client =socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1' ,8083))while True : cmd =input('请输入命令>>:').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8' )) # 接收端 # 1、先手4个字节,从中提取接下来要收的头的长度 x =client.recv(4) header_len =struct.unpack('i',x)[0] # 2、接收头,并解析 json_str_bytes =client.recv(header_len) json_str =json_str_bytes.decode('utf-8') header_dic =json.loads(json_str) print (header_dic) total_size =header_dic["total_size" ] # 3、接收真实的数据 recv_size = 0 while recv_size < total_size: recv_data =client.recv(1024) recv_size+=len(recv_data) print (recv_data.decode('utf-8' ),end ='' ) else : print ()
服务端为多个客户端提供服务 从上面的代码来说,当一个服务器端处于accept()等待连接的时候,一个客户端来了这个时候代码就会继续往下面走,此时如果再来一个客户端,在请求时就会堵塞住,因为服务器端的主线程已经用于服务第一个客户端去了,无法分身服务多个客户端
解决办法:
1.socketserver
2.多线程(多进程)
这两个模块都能解决单进程服务器端的问题,其实就是服务器端把处理客户端的服务交给其他进程或线程取处理,而自己只负责与客户端连接。类似于nginx,nginx一共两个进程,master和slave,master进程仅负责接收客户端的请求,而真正与客户端通信和返回数据的是slave端。
socketserver: 这里用了socketserver模块,直接省略accpet以及以上监听绑定步骤(服务器端),直接用class类继承socketserver.BaseRequestHandler,然后定义handle函数,在恢复和发送数据时调用self.request.(recv or send)即可。最后生成对象,传入要监听的端口ip以及创建好的类名再运行对象即可。
这里只需要创建对象时传入bind的ip+port,已经通信时调用request方法。极其简单便捷。客户端无需改变,该如何通信还是一样,服务器端除了于客户端通信的那段代码其他均为固定格式
服务器端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import subprocess import socketserver import struct class MyRequestHandle(socketserver.BaseRequestHandler): def handle(self ): while True: try: res=self .request.recv(1024 ) if len (res) == 0 :break obj=subprocess.Popen(res.decode('utf-8' ), shell=True, stdout =subprocess.PIPE, stderr =subprocess.PIPE) stdout =obj.stdout .read () stderr =obj.stderr .read () total_size=len (stdout )+len (stderr ) #stdout 为命令执行成功输出,stderr 为执行错误的输出 header=struct.pack('i' ,total_size) self .request.send(header) self .request.send(stdout +stderr ) except Exception: break self .request.close () s=socketserver.ThreadingTCPServer(('127.0.0.1' ,8080 ),MyRequestHandle) s.serve_forever()
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import structfrom socket import *client =socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1' ,8080))while True : msg =input('请输入命令').strip() if len(msg) == 0:continue client.send(msg.encode('utf-8' )) header =client.recv(4) total_size =struct.unpack('i',header)[0] recv_size =0 cmd_res =b'' while recv_size < total_size: data =client.recv(1024) recv_size+=len(data) cmd_res+=data print (data.decode('gbk' ),end ='' ) else : print ('' )
服务器端代码模板: 1 2 3 4 5 6 7 8 9 10 import subprocessimport socketserverimport structclass MyRequestHandle (socketserver .BaseRequestHandler ): def handle(self ): while True : 主代码(于客户端通信的代码) self.request.close() #(关闭套接字代码) s=socketserver.ThreadingTCPServer (('127.0.0.1',8080),MyRequestHandle ) s.serve_forever()
多线程实现: 多线程与多进程都可以实现,这里导入多线程模块,把accpet接收到的客户端对象以下代码全部交与线程来做就可以了。(多进程也可以实现,但一般用多线程即可。)这里用threading模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import socket import threading import struct import subprocess def handle_client_request(ip_port, new_client): print("客户端的ip和端口号为:" , ip_port) while True: recv_data = new_client.recv(1024 ) if recv_data == 'quit' : new_client.close () break elif recv_data: obj = subprocess.Popen(recv_data.decode('utf-8' ), shell =True, stdout =subprocess.PIPE, stderr =subprocess.PIPE) stdout = obj.stdout .read () stderr = obj.stderr .read () total_size = len (stdout ) + len (stderr ) total_size = struct.pack("i" ,total_size) new_client.send (total_size) new_client.send (stdout ) new_client.send (stderr ) else : breakif __name__ == '__main__' : tcp_server_socket = socket .socket (socket .AF_INET, socket .SOCK_STREAM) tcp_server_socket.setsockopt(socket .SOL_SOCKET, socket .SO_REUSEADDR, True) tcp_server_socket.bind(("" , 8080 )) tcp_server_socket.listen(128 ) while True: new_client, ip_port = tcp_server_socket.accept() sub_thread = threading.Thread(target=handle_client_request, args=(ip_port, new_client)) sub_thread.setDaemon(True) sub_thread.start ()
上面为服务端代码,客户端不变,仅为实现服务端多线程
模板: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import socketimport threadingimport structimport subprocess def handle_client_request(ip_port, new _client ): while True: #主代码部分 if __name__ == '__main__' : tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) tcp_server_socket.bind(("" , 8080 )) tcp_server_socket.listen(128 ) while True: new _client , ip_port = tcp_server_socket.accept() sub_thread = threading.Thread(target=handle_client_request, args=(ip_port, new _client )) sub_thread.setDaemon(True) sub_thread.start() # tcp_server_socket.close()