网络编程之socket 基于TCP的套接字 TCP解决粘包 发送大文件 socket收发消息原理剖析 基于UDP的套接字 recv与recvfrom的区别 socketserver模块
tcp服务端
ss = socket() #创建服务器套接字
ss.bind() #把地址绑定到套接字
ss.listen() #监听链接
inf_loop: #服务器无限循环
cs = ss.accept() #接受客户端链接
comm_loop: #通讯循环
cs.recv()/cs.send() #对话(接收与发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
tcp客户端
cs = socket() # 创建客户套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通讯循环
cs.send()/cs.recv() # 对话(发送/接收)
cs.close() # 关闭客户套接字
TCP解决粘包
TCP粘包原因:接收方不知道到底要收多少数据
import socket import subprocess import struct import json server = socket.socket() server.bind(('127.0.0.1',8081)) server.listen(5) while True: conn, addr = server.accept() print('连接成功') while True: try: cmd = conn.recv(1024) print('接收成功') # tcp客户端发空,会导致服务端夯住(udp不存在此问题,因为自带报头,为地址和端口的信息) if len(cmd) == 0:break cmd = cmd.decode('utf-8') #利用subprocess开启新进程,可接收命令,并调用shell去执行这个字符串 obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) #res接收subprocess的shell标准化输出和标准化报误信息 res = obj.stdout.read() + obj.stderr.read() #将模块返回的信息的长度,和其他需要提供的信息,做出字典 d = {'file_size':len(res),'info':'xxxxxxx'} #字典序列化 json_d = json.dumps(d) # 1.先制作一个提示客户端,将要发送的字典长度的报头 # (struct模块,将int类型的长度,转化为二进制字符串,只占4字节) header = struct.pack('i',len(json_d)) # header2=struct.pack('i',len(json_d.encode('utf8'))) 与上句效果相同,算的都是bytes长度 # 2.发送字典报头 conn.send(header) # 3.发送字典 conn.send(json_d.encode('utf-8')) # 4.再发真实数据 conn.send(res) # conn.send(obj.stdout.read()) # conn.send(obj.stderr.read()) except ConnectionResetError: break conn.close()
import socket import struct import json client = socket.socket() client.connect(('127.0.0.1',8081)) print('连接成功') while True: msg = input('>>>:').encode('utf-8') # tcp客户端发空,会导致服务端夯住(udp不存在此问题,因为自带报头,为地址和端口的信息) if len(msg) == 0:continue client.send(msg) # 1.先接收字典报头 header_dict = client.recv(4) # 2.解析报头 获取字典的长度 dict_size = struct.unpack('i',header_dict)[0] # 解包的时候一定要加上索引0 # 3.接收字典数据 dict_bytes = client.recv(dict_size) dict_json = json.loads(dict_bytes.decode('utf-8')) # 4.从字典中获取信息 recv_size = 0 real_data = b'' # 必须是 < ,最后一次若本来可以刚好发完 即 recv_size = dict_json,大不了再接收一次 # 若改为 = ,最后一次本来收完,却还满足判断条件,收到的就是空了,会夯住 while recv_size < dict_json.get('file_size'): data = client.recv(1024) real_data += data recv_size += len(data) print(real_data.decode('gbk')) """ 如何将对方发送的数据收干净 """
发送大文件
import socket import os import json import struct server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn,addr = server.accept() while True: try: header_len = conn.recv(4) # 解析字典报头 header_len = struct.unpack('i',header_len)[0] # 再接收字典数据 header_dic = conn.recv(header_len) real_dic = json.loads(header_dic.decode('utf-8')) # 获取数据长度 total_size = real_dic.get('file_size') # 循环接收并写入文件 recv_size = 0 with open(real_dic.get('file_name'),'wb') as f: while recv_size < total_size: data = conn.recv(1024) f.write(data) recv_size += len(data) print('上传成功') except ConnectionResetError as e: print(e) break conn.close()
import socket import json import os import struct client = socket.socket() client.connect(('127.0.0.1',8080)) while True: # 获取电影列表 循环展示 MOVIE_DIR = r'D:python脱产10期视频day25视频' movie_list = os.listdir(MOVIE_DIR) # print(movie_list) for i,movie in enumerate(movie_list,1): print(i,movie) # 用户选择 choice = input('please choice movie to upload>>>:') # 判断是否是数字 if choice.isdigit(): # 将字符串数字转为int choice = int(choice) - 1 # 判断用户选择在不在列表范围内 if choice in range(0,len(movie_list)): # 获取到用户想上传的文件路径 path = movie_list[choice] # 拼接文件的绝对路径 file_path = os.path.join(MOVIE_DIR,path) # 获取文件大小 file_size = os.path.getsize(file_path) # 定义一个字典 res_d = { 'file_name':'性感荷官在线发牌.mp4', 'file_size':file_size, 'msg':'注意身体,多喝营养快线' } # 序列化字典 json_d = json.dumps(res_d) json_bytes = json_d.encode('utf-8') # 1.先制作字典格式的报头 header = struct.pack('i',len(json_bytes)) # 2.发送字典的报头 client.send(header) # 3.再发字典 client.send(json_bytes) # 4.再发文件数据(打开文件循环发送) with open(file_path,'rb') as f: for line in f: client.send(line) else: print('not in range') else: print('must be a number')
# import socket # 把后期经常改动的变量提取出来 # 一般不提倡import*,socket比较特殊 # 都导入进来后就不需要每次socket.来调用方法了 from socket import * ip_port=('127.0.0.1',8083) back_log=5 buffer_size=1024 tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) while True: #循环的等待接收服务 print('服务端开始运行了') conn,addr=tcp_server.accept() #服务端阻塞等待链接 print('双向链接是',conn) #链接信息 print('客户端地址',addr) #地址+端口 while True: #循环的为这个链接服务 try: data=conn.recv(buffer_size) print('客户端发来的消息是',data.decode('utf-8')) conn.send(data.upper()) except Exception: break conn.close() tcp_server.close()
# import socket from socket import * ip_port=('127.0.0.1',8083) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: msg=input('>>: ').strip() if not msg:continue #如果客户端输入为空,重新回到输入 tcp_client.send(msg.encode('utf-8')) print('客户端已经发送消息') data=tcp_client.recv(buffer_size) print('收到服务端发来的消息',data.decode('utf-8')) tcp_client.close() # recv(1024),其实是在内核态内存收消息 # send(msg),其实是消息从用户态内存拷贝到内核态内存
服务端循环链接请求+循环收发消息
堆结构:先进先出,吃了拉 先吃进去的先拉出来 python中的堆结构为队列 栈结构:先进后出,后进先出 吃了吐,后面吃的先吐出来 python中没有专门的栈结构
客户端直接终止:四次挥手都没有,直接中断的,而服务端本来等着收,conn突然被干掉了,recv就没意义了(远程主机强迫关闭了一个现有连接)
C/S架构的美好愿望,服务端启动后,永远运行下去,服务很多人
循环提供服务 如果服务端正在为客户端a提供服务,那么服务端进入 “为a链接服务”的循环中,此时这个循环还没结束,为a链接服务的conn.close()没有执行,回不到上一个循环,即无法接收新的会话连接 此时客户端b试图建立链接,send到自己的内核态内存,并发往服务器,进入服务器的block_log连接池中挂起。 等对a的服务进程完成断开后,进入下一个接收连接循环,服务端的内核态内存才会把挂起的“b连接”发给用户态内存,服务端程序才能接收并进入服务循环
客户端b试图想建立链接,这时候客户端如果发了一条消息,其实发消息就已经进入客户端的循环,只是服务端没有回应,客户端的内核态内存没新数据,故客户端的tcp_client.recv(buffer_size)没有收到信息,自然前面的data= 就没有被赋值,再次就中断等待了
此时手动结束客户端a进程,服务端conn被干掉,报错:远程主机强迫关闭了一个现有连接,服务端进程卡死。故需要在 为a连接服务的 循环,加一个异常处理 ,遇到问题 break,继续运行退出循环
避免通信循环中的报错
1.服务端在conn.recv()下一句
用if 为空 :break 解决conn断开,服务端一直收空消息造成的死循环
2.服务端在通信循环 用处理客户端进程非正常中断造成的报错(远程主机强迫关闭了一个现有的连接)
try:
#通信循环
except Exception as e:
print(e)
break
import socket """ 服务端 固定的ip和port 24小时不间断提供服务 """ server = socket.socket() # 生成一个对象 server.bind(('127.0.0.1',8080)) # 绑定ip和port server.listen(5) # 半连接池 while True: conn, addr = server.accept() # 等到别人来 conn就类似于是双向通道 print(addr) # ('127.0.0.1', 51323) 客户端的地址 while True: try: data = conn.recv(1024) print(data) # b'' 针对mac与linux 客户端异常退出之后 服务端不会报错 只会一直收b'' if len(data) == 0:break conn.send(data.upper()) except ConnectionResetError as e: print(e) break conn.close()
import socket client = socket.socket() client.connect(('127.0.0.1',8080)) while True: msg = input('>>>:').encode('utf-8') if len(msg) == 0:continue client.send(msg) data = client.recv(1024) print(data)
recv谁发起的:由用户态内存中的应用程序发起的,可以是客户端,可以使服务端 最多收到设定的字节数
TCP服务端防止收空
若客户端直接回车,即发了一个None,客户端send空到客户端内核态内存 而服务端的内核态缓存,没东西。,自然卡住了,后面的send就不会执行。客户端在send空后,会一直等着收内核态缓存的信息,而客户端没收到新消息,所以也卡住了
卡住recv 是由于:内核态缓存没东西,跟对方没关系
linux、unix遇到客户端终端,服务端不会抛出异常,而是recv会一直接收None,跟windows 不一样
解决方法:
while True:
#循环的为这个链接服务
data=conn.recv(buffer_size)
if not data:break #如果收到的是空,退出循环
print('客户端发来的消息是',data.decode('utf-8'))
conn.send(data.upper())
conn.close()
基于UDP的套接字
UDP服务端
ss = socket() #创建一个服务器的套接字
ss.bind() #绑定服务器套接字
inf_loop: #服务器无限循环
cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
ss.close() # 关闭服务器套接字
没有listen:listen是用来挂连接请求的,半链接池,UDP没有链接 少了一个链接循环:不需要链接
UDP客户端
cs = socket() # 创建客户套接字
comm_loop: # 通讯循环
cs.sendto()/cs.recvfrom() # 对话(发送/接收)
cs.close() # 关闭客户套接字
没有绑定:直接创建套接字,不需要绑定 只需要通讯循环
UDP服务端
from socket import * ip_port=('127.0.0.1',8080) buffer_size=1024 udp_server=socket(AF_INET,SOCK_DGRAM) #数据报 udp_server.bind(ip_port) while True: data,addr=udp_server.recvfrom(buffer_size) #客户端发的是(二进制内容,ip_port),所以收到的是元组 print(data) udp_server.sendto(data.upper(),addr)
from socket import * ip_port=('127.0.0.1',8080) #服务端的 buffer_size=1024 udp_client=socket(AF_INET,SOCK_DGRAM) #数据报 while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) #没有链接,每次要指定发给哪个ip端口 #发的是(二进制内容,ip_port)的元组 data,addr=udp_client.recvfrom(buffer_size) # 收到的也是(二进制内容,ip_port)的元组 # print(data.decode('utf-8')) print(data)
recv与recvfrom的区别
tcp收和udp收,都是从自己的缓冲区拿内容,tcp收不到空内容,udp却可以收到(None,ip_port)元组 recv在自己的缓冲区为空时,阻塞 recvfrom在收到一个空内容时,虽然内容为空,但是带了ip_port的报文头
TCP 一端发完一个包后,收到对面的ACK确认收到响应,才会在内核缓冲区把数据清空 socket用户态内存发给内核态内存,本质是copy操作
UDP不依赖于服务端
import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): # print('来啦 老弟') while True: data = self.request.recv(1024) print(self.client_address) # 客户端地址 print(data.decode('utf-8')) self.request.send(data.upper()) if __name__ == '__main__': """只要有客户端连接 会自动交给自定义类中的handle方法去处理""" server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer) # 创建一个基于TCP的对象 server.serve_forever() # 启动该服务对象
连续开多个客户端进行验证
import socket client = socket.socket() client.connect(('127.0.0.1',8080)) while True: client.send(b'hello') data = client.recv(1024) print(data.decode('utf-8'))
UDP的socketserver
import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): # print('来啦 老弟') while True: data,sock = self.request print(self.client_address) # 客户端地址 print(data.decode('utf-8')) sock.sendto(data.upper(),self.client_address) if __name__ == '__main__': """只要有客户端连接 会自动交给自定义类中的handle方法去处理""" server = socketserver.ThreadingUDPServer(('127.0.0.1',8080),MyServer) # 创建一个基于TCP的对象 server.serve_forever() # 启动该服务对象
import socket import time client = socket.socket(type=socket.SOCK_DGRAM) server_address = ('127.0.0.1',8081) while True: client.sendto(b'hello',server_address) data,addr = client.recvfrom(1024) print(data.decode('utf-8'),addr) #睡一秒,不然速度太快,收不到其余客户端的消息 time.sleep(1)