网络编程之基于tcp协议和udp协议的套接字通信
一、套接字socket
1、在任何类型的通信开始之前,网络应用程序都必须创建套接字。
2、socket一般指套接字,套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信(Inter Process Communication,IPC),随着socket的不断更新。现在可以实现不同主机之间进行通信的工具。
3、有两种类型的套接字:基于文件的和面向网络的
基于文件的套接字
家族名:AF_UNIX,(又名AF_LOCAL,在POSIX1.g标准中指定),它代表地址家族(addressfamily):UNIX。
其他比较旧的系统可能会将地址家族表示成域(domain)或协议家族(protocolfamily),并使用其缩写PF而非AF。
类似地,AF_LOCAL(在2000~2001年标准化)将代替AF_UNIX
基于网络的套接字
家族名:AF_INET,或者地址家族:因特网。另一个地址家族AF_INET6用于第6版因特网协议(IPv6)寻址。
此外,还有其他的地址家族,在所有的地址家族之中,目前AF_INET是使用得最广泛的
4、面向连接的套接字与无连接套接字
面向连接的套接字:TCP套接字的名字SOCK_STREAM,特点:可靠,开销大
无连接套接字:UDP套接字的名字SOCK_DGRAM,特点:不可靠(局网内还是比较可靠的),开销小
二、基于tcp协议的套接字通信
简单版:
1、客户端
import socket #1、整个手机 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议 #2、拨通服务端电话 phone.connect(('127.0.0.1',8081)) #3、通信 import time time.sleep(10) phone.send('hello egon 哈哈哈'.encode('utf-8')) #发送数据 data=phone.recv(1024) #接收服务端返回数据 print(data.decode('utf-8')) #4、关闭连接(必选的回收资源的操作) phone.close()
2、服务端
import socket # 1、买手机 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议 # 2、绑定手机卡 phone.bind(('127.0.0.1',8081)) # 0-65535, 1024以前的都被系统保留使用 # 3、开机 phone.listen(5) # 5指的是半连接池的大小 print('服务端启动完成,监听地址为:%s:%s' %('127.0.0.1',8080)) # 4、等待电话连接请求:拿到电话连接conn conn,client_addr=phone.accept() # print(conn) print("客户端的ip和端口:",client_addr) # 5、通信:收\发消息 data=conn.recv(1024) # 最大接收的数据量为1024Bytes,收到的是bytes类型 print("客户端发来的消息:",data.decode('utf-8')) conn.send(data.upper()) # 6、关闭电话连接conn(必选的回收资源的操作) conn.close() # 7、关机(可选操作) phone.close()
加上通信循环版:
1、客户端
import socket #1、买手机 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议 #2、拨通服务端电话 phone.connect(('127.0.0.1',8083)) #3、通信 while True: msg=input("输入要发送的消息>>>: ").strip() #msg='' if len(msg) == 0:continue phone.send(msg.encode('utf-8')) print('======?') data=phone.recv(1024) print(data.decode('utf-8')) #4、关闭连接(必选的回收资源的操作) phone.close()
2、服务端
import socket # 1、买手机 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议 # 2、绑定手机卡 phone.bind(('127.0.0.1',8083)) # 0-65535, 1024以前的都被系统保留使用 # 3、开机 phone.listen(5) # 5指的是半连接池的大小 print('服务端启动完成,监听地址为:%s:%s' %('127.0.0.1',8080)) # 4、等待电话连接请求:拿到电话连接conn conn,client_addr=phone.accept() # 5、通信:收\发消息 while True: try: data=conn.recv(1024) # 最大接收的数据量为1024Bytes,收到的是bytes类型 if len(data) == 0: # 在unix系统洗,一旦data收到的是空 # 意味着是一种异常的行为:客户度非法断开了链接 break print("客户端发来的消息:",data.decode('utf-8')) conn.send(data.upper()) except Exception: # 针对windows系统 break # 6、关闭电话连接conn(必选的回收资源的操作) conn.close() # 7、关机(可选操作) phone.close()
加上链接循环:同上,打开多个服务端,一个客户端来实现
基于tcp协议实现远程执行命令
粘包问题:当send一条信息时,无论底层怎样分段分片,TCP协议层会把构成整条信息的数据段排序完成后才呈现在内核缓冲区。即面向流的通信是无消息保护边界的。所以粘包问题是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。只有tcp有粘包问题,而udp没有粘包问题
1、客户端
from socket import * client=socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8082)) while True: cmd=input('请输入命令>>:').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8')) # 解决粘包问题思路: # 1、拿到数据的总大小total_size # 2、recv_size=0,循环接收,每接收一次,recv_size+=接收的长度 # 3、直到recv_size=total_size,结束循环 cmd_res=client.recv(1024) # 本次接收,最大接收1024Bytes print(cmd_res.decode('utf-8')) # 强调:windows系统用gbk # 粘包问题出现的原因 # 1、tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分 # 2、收数据没收干净,有残留,就会下一次结果混淆在一起 # 解决的核心法门就是:每次都收干净,不要任何残留
2、服务端
# 服务端应该满足两个特点: # 1、一直对外提供服务 # 2、并发地服务多个客户端 import subprocess from socket import * server=socket(AF_INET,SOCK_STREAM) server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加#在绑定前调用setsockopt 让套接字允许地址重用 server.bind(('127.0.0.1',8082)) 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() print(len(stdout_res)+len(stderr_res)) # conn.send(stdout_res+stderr_res) # ??? conn.send(stdout_res) conn.send(stderr_res) # with open("1.mp4",mode='rb') as f: # for line in f: # conn.send(line) except Exception: break conn.close()
粘包问题的解决
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是发送端在发送数据前,发一个头文件包,告诉发送的字节流总大小,然后接收端来一个死循环接收完所有数据
使用struct模块可以用于将Python的值根据格式符,转换为字符串(byte类型)
struct模块中最重要的三个函数是pack(), unpack(), calcsize()
pack(fmt, v1, v2, ...) 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)
unpack(fmt, string) 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
calcsize(fmt) 计算给定的格式(fmt)占用多少字节的内存
1、客户端的解决
import struct import json from 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()
2、服务端解决
import subprocess import struct import json from socket import * server=socket(AF_INET,SOCK_STREAM) server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 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) # 1、制作头 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') # 2、先把头的长度发过去 x=struct.pack('i',len(json_str_bytes)) conn.send(x) # 3、发头信息 conn.send(json_str_bytes) # 4、再发真实的数据 conn.send(stdout_res) conn.send(stderr_res) except Exception: break conn.close()
三、基于udp协议的套接字通信
1、客户端
import socket client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 流式协议=》tcp协议 while True: msg=input('>>>: ').strip() client.sendto(msg.encode('utf-8'),('127.0.0.1',8081)) res=client.recvfrom(1024) print(res) client.close()
2、服务端
import socket server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 数据报协议=》udp协议 server.bind(('127.0.0.1',8081)) while True: data,client_addr=server.recvfrom(1024) server.sendto(data.upper(),client_addr) server.close()
udp协议没有粘包问题:
1、客户端
import socket client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) client.sendto(b'hello',('127.0.0.1',8080)) client.sendto(b'world',('127.0.0.1',8080)) client.close()
2、服务端
import socket server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) server.bind(('127.0.0.1',8080)) res1=server.recvfrom(2) # b"hello" print(res1) res2=server.recvfrom(3) # b"world" print(res2) server.close()
四、socket 之 send 和 recv 原理剖析
1、TCP socket 的发送和接收缓冲区
当创建一个 TCP socket 对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存中的一片空间。
2、 send 原理剖析
send不是直接把数据发给服务端,要想发数据,必须得通过网卡发送数据,应用程序是无法直接通过网卡发送数据的,它需要调用操作系统接口,也就是说,应用程序把发送的数据先写入到发送缓冲区 (内存中的一片空间),再由操作系统控制网卡把发送缓冲区的数据发送给服务端网卡 。
3、 recv 原理剖析
recv 不是直接从客户端接收收数据,应用软件是无法直接通过网卡接收数据的,它需要调用操作系统接口,由操作系统通过网卡接收数据,把接收的数据写入到接收缓冲区 (内存中的一片空间),应用程序再从接收缓存区获取客户端发送的数据。
4、 send 和 recv 原理剖析图
说明:
-
发送数据是发送到发送缓冲区
-
接收数据是从接收缓冲区获取
五、socketserver模块的简单使用
Socketserver内部使用IO多路复用以及“多线程”和“多进程”,从而实现并发处理多个客户端请求的socket服务端。 即,每个客服端请求连接到服务器时,socket服务端都会在服务器上创建一个“线程”或“进程”专门负责处理当前客户端的所有请求。
ThreadingTCPServer实现的socket服务器内部会为每个client创建一个“线程”,该线程用来和客户端就行交互 。
比如服务端代码
import socketserver class MyRequestHandle(socketserver.BaseRequestHandler): ## 必须继承BaseRequestHandler def handle(self): #必须有handle方法 # 如果tcp协议,self.request=>conn print(self.client_address) while True: try: msg = self.request.recv(1024) if len(msg) == 0: break self.request.send(msg.upper()) except Exception: break self.request.close() # 服务端应该做两件事 # 第一件事:循环地从半连接池中取出链接请求与其建立双向链接,拿到链接对象 s=socketserver.ThreadingTCPServer(('127.0.0.1',8889),MyRequestHandle) #实例化对象,实现多线程的socket s.serve_forever() #事件监听,并调用MyRequestHandle方法
# 等同于 # while True: # conn,client_addr=server.accept() # 启动一个线程(conn,client_addr) # 第二件事:拿到链接对象,与其进行通信循环===>handle
服务端这样设置后,就能和多个客户端通信。