一、TCP/IP 五层模型讲解
互联网协议按照功能不同分为tcp/ip四层或tcp/ip五层或osi七层
每层运行常见物理设备
物理层:高电压、低电压
数据链路层:定义了电信号的分组方式、以太网协议、mac地址
网络层:IP协议、子网掩码、arp协议功能:广播的方式发送数据包,获取目标主机的mac地址
传输层:建立端口到端口的通信
应用层:规定应用程序的数据格式
三次握手四次挥手
三次握手:
1.客户端向服务端发送SYN seq=x请求链接
2.服务端回ACK=x+1 并且同时发起SYN seq=y 的请求链接
3.客户端回 ACK=Y+1给服务端同意请求链接
四次挥手:
1.客户端向服务端发送FIN seq=x+2 请求断开
2.服务端回ACK=x+3 同意断开
3.服务端向客户端端发送FIN seq=y+1 请求断开
4.客户端向服务端回ACK=y+2同意断开
二、socket的定义
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口
也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序,而程序的pid是同一台机器上不同进程或者线程的标识。
三、socket套接字工作流程
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
import socket socket.socket(socket_family,socket_type,protocal=0) socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。 获取tcp/ip套接字 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 获取udp/ip套接字 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。 例如tcpSock = socket(AF_INET, SOCK_STREAM)
套接字函数
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
四、基于TCP的套接字
TCP服务端
phone = socket() #创建服务器套接字 phone.bind() #把地址绑定到套接字 phone.listen() #监听链接 inf_loop: #服务器无限循环 cs = phone.accept() #接受客户端链接 comm_loop: #通讯循环 phone.recv()/cs.send() #对话(接收与发送) cs.close() #关闭客户端套接字 phone.close() #关闭服务器套接字(可选)
TCP客户端
cs = socket() # 创建客户套接字 cs.connect() # 尝试连接服务器 comm_loop: # 通讯循环 cs.send()/cs.recv() # 对话(发送/接收) cs.close() # 关闭客户套接字
举例
#_*_coding:utf-8_*_ import socket ip_port=('127.0.0.1',8080) #电话卡 buf_size=1024 #收发消息的尺寸 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机 phone.bind(ip_port) #监听端口 phone.listen(5) #挂起链接数5 conn,addr=phone.accept() #手机接电话 # print(conn) # print(addr) print('接到来自%s的电话' %addr[0]) msg=conn.recv(buf_size) #听消息,听话 print(msg,type(msg)) conn.send(msg.upper()) #发消息,说话 conn.close() #挂电话 s.close() #手机关机
#_*_coding:utf-8_*_ import socket ip_port=('127.0.0.1',8080) buf_size=1024 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect_ex(ip_port) #拨电话 s.send('hello'.encode('utf-8')) #发消息,说话(只能发送字节类型) feedback=phone.recv(BUFSIZE) #收消息,听话 print(feedback.decode('utf-8')) phone.close() #挂电话
改进版:实现服务端不断接受链接,然后循环通信,通信完毕后只关闭链接,服务器能够继续接收下一次链接。
服务端
import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #获取套接字实例 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #重用IP和端口 phone.bind(('127.0.0.1',8080)) #监听IP和端口 phone.listen(5) #挂起链接数 print('starting...') while True: conn,addr=phone.accept() #等待链接 print('conn是{0}'.format(conn)) print('端口号是{0}'.format(addr)) while True: try: data=conn.recv(1024) #从内存中读取收到的消息 print('客户端发来的消息是%s'%data) conn.send(data.upper()) #向内存中发送消息 except Exception: break conn.close() #断开线路
客户端
import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #获取套接字实例 phone.connect(('127.0.0.1',8080)) #链接端口 while True: msg=input('>>:').strip() if not msg:continue phone.send(msg.encode('utf-8')) #编码成字节再发送 data=phone.recv(1024) print(data)
五、基于UDP的套接字
举例
UDP服务端
#_*_coding:utf-8_*_ import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server_client.bind(ip_port) while True: msg,addr=udp_server_client.recvfrom(BUFSIZE) print(msg,addr) udp_server_client.sendto(msg.upper(),addr)
UDP客户端
#_*_coding:utf-8_*_ import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>: ').strip() if not msg:continue udp_server_client.sendto(msg.encode('utf-8'),ip_port) back_msg,addr=udp_server_client.recvfrom(BUFSIZE) print(back_msg.decode('utf-8'),addr)
#_*_coding:utf-8_*_ from socket import * from time import strftime ip_port=('127.0.0.1',9000) bufsize=1024 tcp_server=socket(AF_INET,SOCK_DGRAM) tcp_server.bind(ip_port) while True: msg,addr=tcp_server.recvfrom(bufsize) print('===>',msg) if not msg: time_fmt='%Y-%m-%d %X' else: time_fmt=msg.decode('utf-8') back_msg=strftime(time_fmt) tcp_server.sendto(back_msg.encode('utf-8'),addr) tcp_server.close()
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',9000) bufsize=1024 tcp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('请输入时间格式(例%Y %m %d)>>: ').strip() tcp_client.sendto(msg.encode('utf-8'),ip_port) data=tcp_client.recv(bufsize) print(data.decode('utf-8')) tcp_client.close()
六、粘包现象
服务端
from socket import * import subprocess ip_port=('127.0.0.1',8080) buf_size=1024 tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) #链接循环 while True: conn,addr=tcp_socket_server.accept() print('客户端',addr) #通信循环 while True: try: cmd=conn.recv(buf_size) if len(cmd)==0:break res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) conn.send(res.stdout.read()) conn.send(res.stderr.read()) except Exception: break conn.close()
客户端
import socket buf_size=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) while True: cmd=input('>>:').strip() if len(cmd)==0:continue if cmd=='quit':break s.send(cmd.encode('utf-8')) act_res=s.recv(buf_size) print(act_res.decode('gbk'),end='')
1.TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
3.tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
粘包情况一:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包) 网络延迟大于程序内部运行时间
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send(world'.encode('utf-8'))
粘包情况二:接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(2) #一次没有收完整 data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello world'.encode('utf-8'))
七、解决粘包方法
import json,struct #假设通过客户端上传1T:1073741824000的文件a.txt #为避免粘包,必须自定制报头 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值 #为了该报头能传送,需要序列化并且转为bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度 #客户端开始发送 conn.send(head_len_bytes) #先发报头的长度,4个bytes conn.send(head_bytes) #再发报头的字节格式 conn.sendall(文件内容) #然后发真实内容的字节格式 #服务端开始接收 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式 header=json.loads(json.dumps(header)) #提取报头 #最后根据报头的内容提取真实的数据,比如 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
json报头
服务端
from socket import * import subprocess,struct,json ip_port=('127.0.0.1',8080) buf_size=1024 tcp_socket_server=socket(AF_INET,SOCK_STREAM) #生成套接字实例 tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #开启重用端口功能 tcp_socket_server.bind(ip_port) #绑定端口 tcp_socket_server.listen(5) #开始监听并设置端口数量 #链接循环 while True: conn,addr=tcp_socket_server.accept() #等待链接 print('客户端',addr) #通信循环 while True: try: cmd=conn.recv(buf_size) #接收数据 if len(cmd)==0:break #数据为空时,结束掉接收过程,回到等待链接过程 res=subprocess.Popen(cmd.decode('utf-8'),shell=True, #把数据扔到管道内执行 stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) out_res=res.stdout.read() #读取正常执行结果的内容 err_res=res.stderr.read() #读取异常错误的内容 data_size=len(out_res)+len(err_res) #把正常内容和异常内容的字节数加起来 head_dic={'data_size':data_size} #报头封装成字典形式 head_json=json.dumps(head_dic) #把封装的字典dumps成字符串 head_bytes=head_json.encode('utf-8') #把字符串的字典编码成字节 #part1 报头长度 head_len=len(head_bytes) #获取报头字节长度 conn.send(struct.pack('i',head_len)) #报头字节长度用struct.pack打包成字节,再发送报头长度 #part2 发送报头 conn.send(head_bytes) #part3 发送数据部分 conn.send(out_res) conn.send(err_res) except Exception: break conn.close()
客户端
import socket,struct,json buf_size=1024 ip_port=('66.112.219.21',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #实例化生成一个套接字 res=s.connect_ex(ip_port) #向指定IP 端口 发起链接请求 while True: cmd=input('>>:').strip() #等待用户输入 if len(cmd)==0:continue #用户输入为空时 返回让用户继续输入 if cmd=='quit':break #当输入内容为quit时 结束链接请求 s.send(cmd.encode('utf-8')) #发送命令 #part1 先收报头长度 head_struct=s.recv(4) head_len=struct.unpack('i',head_struct)[0] #struct.unpack解包获得报头长度 #part2 收报头 head_bytes=s.recv(head_len) #获得字节形式的报头 head_json=head_bytes.decode('utf-8') #字节报头解码成json字符串 head_dic=json.loads(head_json) #json.loads反序列化字符串得出报头字典 print(head_dic) #打印出报头字典 data_size=head_dic['data_size'] #回去报头字典中数据字节数 #part3 收数据 recv_size=0 #定义计数器 recv_data=b'' #定义拼接字节的变量 while recv_size < data_size: #当计数器小于数据字节数时 data=s.recv(1024) #每次收1024个字节 recv_size+=len(data) #计数器把每次循环收的字节长度加起来,记录已完成读取数据的进度 recv_data+=data #把每次获取到的1024个字节拼接起来 print(recv_data.decode('gbk'),end='') #因为subprocess的原因,windows下解码用'gbk',linux下用'utf-8' s.close()
八、socketserver实现并发
服务端
import socketserver,struct,json,subprocess,os class My_TCP_server(socketserver.BaseRequestHandler): max_packet_size=1024 #最大接收字节数 coding='utf-8' #解码方式 BASE_DIR=os.path.dirname(os.path.abspath(__file__)) server_dir='file_upload' #服务端用于上传文件存放的文件夹名 def handle(self): while True: try: head_struct=self.request.recv(4) #接收报头长度 if not head_struct:break head_len=struct.unpack('i',head_struct)[0] #获得报头长度 head_json=self.request.recv(head_len).decode(self.coding) #获得报头并解码成json字符串 head_dic=json.loads(head_json) #json字符串再loads成报头字典 print(head_dic) #head_dic={'cmd':'put','filename':'a.txt','filesize':556632} cmd=head_dic['cmd'] #取出报头字典的cmd命令 if hasattr(self,cmd): #如果实例或类中定义了相关的cmd命令函数 func=getattr(self,cmd) #拿到命令的内存地址并赋给func func(head_dic) #在执行func的时候把报头字典传进去 except Exception: #发生任何异常,则终止链接 break def put(self,args): file_path=os.path.normpath(os.path.join(self.BASE_DIR, self.server_dir, args['filename'])) #拼接服务端存放上传文件的路径 filesize=args['filesize'] #取出上传文件的数据大小值 recv_size=0 #计数器 print('--->',file_path) with open(file_path,'wb') as f: while recv_size<filesize: #计数器数值小于文件数据大小值时 recv_data=self.request.recv(self.max_packet_size) #一次循环接收的字节 f.write(recv_data) #写入 recv_size+=len(recv_data) #计数器数值+已写入数值 print('recvsize:%s filesize:%s'%(recv_size,recv_data)) if __name__=='__main__': obj=socketserver.ThreadingTCPServer(('127.0.0.1',8081),My_TCP_server) #帮你绑定和监听 obj.serve_forever() #拿到conn,addr,保持链接循环
客户端
import socket,struct,json,os class My_TCP_client: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address=False #是否重用IP和端口,默认不重用 max_packet_size=1024 #最大接收字节数 coding='utf-8' #编码方式 request_queue_size=5 #挂起链接数5 def __init__(self,server_address,connect=True): self.server_address=server_address self.socket=socket.socket(self.address_family,self.socket_type) if connect: try: self.client_connect(server_address) except Exception: self.client_close() def client_connect(self,server_address): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: ww=input('>>:').strip() if not ww:continue x=ww.split() cmd=x[0] if hasattr(self,cmd): func=getattr(self,cmd) func(x) def put(self,args): cmd=args[0] #拿到执行命令 filename=args[1] #拿到文件路径名 if not os.path.isfile(filename): #判断文件是否存在 print('file:%s is not exists'%filename) return else: filesize=os.path.getsize(filename) #获取文件大小 head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} #封装报头字典 print(head_dic) head_json=json.dumps(head_dic) #把报头字典dumps成json字符串 head_json_bytes=bytes(head_json,encoding=self.coding) #json字符串编码成字节格式,等待被send head_struct=struct.pack('i',len(head_json_bytes)) #封装报头长度 self.socket.send(head_struct) #发送报头长度 self.socket.send(head_json_bytes) #发送报头数据 send_size=0 #计数器归零 with open(filename,'rb') as f: #以读的方式打开文件 for i in f: #每次读一行 self.socket.send(i) #发送一行 send_size += len(i) #计数器数值+已发送的数据大小 print(send_size) else: print('upload successful') #for 循环正常结束后打印 已完成信息 client=My_TCP_client(('127.0.0.1',8081)) client.run()