1. socket套接字
1.1 socket简介
1)网络中的进程间通信
在本地可以通过进程PID来唯一标识一个进程,但是在网络中这却是行不通的。
TCP/IP协议族已经解决了这个问题,网络层的“IP地址”可以唯一标识网络中的主机,而传输层“协议+端口”可以唯一标识主机中的应用程序(进程)。
这样利用IP地址,协议,端口 就可以表示网络中的进程了,网络中的进程通信就可以利用这个标志与其他进程进行交互。
2)什么是socket
socket简称套接字,是进程间通信的一种方式;它能实现不同主机间的进程间通信。
socket是应用层与TCP/IP协议族通信中间软件抽象层,它是一组接口。
在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口后面,对于用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议;进而,我们无需去关心TCP/UDP协议的细节,因为socket已经封装好了,我们只需要遵循socket的规定去编程,自然就是遵循tcp/udp标准的。
3)socket层
1.2 套接字的分类
基于文件类型的套接字家族:AF_UNIX
在泛unix系统上,基于文件的套接字调用的就是底层的文件系统来取数据,
两个套接字进程运行在同一机器上,可以通过访问同一个文件来间接完成通信。
基于网络类型的套接字家族:AF_INET
还有AF_INET6被用于ipv6
1.3 套接字的工作流程
1)工作流程图示
2)工作流程解释
服务器端:服务端先初始化socket,然后与端口绑定(bind),对端口进行监听(listen),再调用accept阻塞,等待客户端连接
客户端:服务端初始化完毕后,如果有个客户端初始化一个socket,然后连接服务器(connect),如果简连接成功,这时客户端与服务端的连接就建立了
客户端与服务端进行交互:客户端发送数据请求,服务端接受请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
1.4 创建socket
1)创建socket示例
socket_family:可以选择AF_INET或者AF_UNIX
socket_type:可以选择SOCK_STREAM(流式套接字,主要用于TCP协议)或者SOCK_DGRAM(数据报套接字,主要用于UDP协议)
importsocket
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)
2)服务端套接字函数
s.bind() #绑定(主机,端口号)到套接字
s.listen() #开始TCP监听
s.accept() #被动接受TCP客户的连接,(阻塞式)等待连接的到来
3)客户端套接字函数
s.connect() #主动初始化TCP服务器连接
s.connect_ex() #connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
4)公共用途的套接字函数
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() #关闭套接字
5)面向锁的套接字方法
s.setblocking() #设置套接字的阻塞与非阻塞模式
s.settimeout() #设置阻塞套接字操作的超时时间
s.gettimeout() #得到阻塞套接字操作的超时时间
6)面向文件的套接字的函数
s.fileno() #套接字的文件描述符
s.makefile() #创建一个与该套接字相关的文件
2. 基于TCP的套接字
2.1 简单的tcp服务端&客户端
tcp是基于连接的,必须先启动服务端,然后再启动客户端去连接服务端。
1)socket服务端
importsocket#创建tcp套接字
phone =socket.socket(socket.AF_INET, socket.SOCK_STREAM)#绑定地址,这里接受的参数是一个元组的形式
phone.bind(('127.0.0.1', 8000))#监听连接
phone.listen(5)#此处的listen中的值表示处于半连接和已连接状态的client总和#等待中的半连接和已连接都保存在backlog中,此处的linsten的数量相当于就是在指定backlog的大小
# 如果当前已建立连接数和半连接数达到设定值,那么新客户端就不会connect成功,而是等待服务器
#接受客户端连接,在这个位置等待接收客户端发送的消息
conn, addr =phone.accept()#conn表示为这个客户端创建出了包含tcp三次握手信息的新的套接字#addr 包含这个客户端的信息
msg= conn.recv(1024) #接受客户端发来的消息
print('客户端发来的消息是:', msg)
conn.send(msg.upper())#将接受到的消息转换成大写的形式再发给客户端
conn.close()#触发四次挥手
phone.close() #关闭socket
2)socket客户端
importsocket
phone=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8000)) #链接服务端的相应端口下的应用
phone.send('hello'.encode('utf-8')) #接受和发送消息时数据都应该是字节格式
data= phone.recv(1024)print('收到服务端发来的消息:', data)
phone.close()
2.2 循环收发消息
1)socket服务端(循环收发)
接收消息的的本质其实是在本机的内核空间内提取内容
发送消息的本质其实是将消息内容由用户空间发送到内核空间,然后由内核来完成消息的发送
from socket import *tcp_server=socket(AF_INET, SOCK_STREAM)
ip_port= ('127.0.0.1', 9999)
listen_buffer= 5recv_buffer= 1024tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR,1) #解决端口重用的问题tcp_server.bind(ip_port)
tcp_server.listen(listen_buffer)whileTrue:print('服务端开始运行----->')
conn, addr=tcp_server.accept()whileTrue:try:print('conn----->',conn)print('addr----->',addr)
date= conn.recv(recv_buffer) #这里的收消息其实是在本机的内核空间内提取内容
print(date.decode('utf-8'))
conn.send('hello world!?'.encode('utf-8')) #发消息的本质是将消息内容由用户空间发送到内核空间,然后由内核来完成消息的发送
#conn.sendall(data) # sendall函数可以将一整个数据循环着发送
except Exception: #捕获当客户端意外断开时的异常,若发生异常,则退出内循环,而重新尝试连接
breakconn.close()
tcp_server.close()
2)socket客户端(循环收发)
from socket import *tcp_client=socket(AF_INET, SOCK_STREAM)
ip_port= ('127.0.0.1', 9999)
buffer_size= 1024tcp_client.connect(ip_port)whileTrue:
msg= input("请输入要发送的内容:").strip()if not msg : continuetcp_client.send(msg.encode("utf-8")) #这里如果发送的消息为空(直接敲回车),则没有任何内容发往本机内核空间,所以消息根本就不会发送
data=tcp_client.recv(buffer_size)print("接收到的消息为:", data.decode('utf-8'))
tcp_client.close()
2.3 关于端口重用
1)关于Address already in use的报错
由于服务端仍然存在四次挥手的time_wait状态在占用地址
服务器高并发情况下会有大量的time_wait状态的优化方法
2)端口重用的解决方法一:添加socket配置
#加一条socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #在bind前加上这一句
phone.bind(('127.0.0.1',8080))
3)端口重用的解决方法二:修改内核参数
#发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf#编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1net.ipv4.tcp_tw_reuse= 1net.ipv4.tcp_tw_recycle= 1net.ipv4.tcp_fin_timeout= 30
#然后执行 /sbin/sysctl -p 让参数生效
#参数说明
net.ipv4.tcp_syncookies = 1 #表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;net.ipv4.tcp_tw_reuse= 1 #表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;net.ipv4.tcp_tw_recycle= 1 #表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。net.ipv4.tcp_fin_timeout#修改系統默认的 TIMEOUT 时间
3. 基于UDP的套接字
3.1 UDP简介
1)UDP
UDP(用户数据保协议)是一个无连接的简单面向数据报的传输层协议;
UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地;
由于UDP在传输数据前不用在客户端和服务端之间建立一个连接,且没有超时重发等机制,故而传输速度很快;
UDP是一种面向无连接的协议,每一个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
2)UDP的特点
UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口信息,由于通讯不需要连接,所以可以实现广播发送;
UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内;
UDP是一个不可靠的协议,发送方所发送的数据并不一定以相同的次序到达接收方。
3)适用情况
UDP一般多用于多点通信和实时的数据业务,它注重速度流畅。
语⾳⼴播
视频
TFTP(简单⽂件传送)
SNMP(简单⽹络管理协议)
RIP(路由信息协议,如报告股票市场,航空信息)
DNS(域名解释)
3.2 UDP的通信过程
1)图示
创建客户端套接字
发送/接收数据
关闭套接字
2)UDP是无链接的,先启动哪一端都不会报错
3)udp的服务器和客户端的区分
UDP的服务端和客户端往往是通过 请求服务 和 提供服务 来进⾏区分
请求服务的⼀⽅称为:客户端
提供服务的⼀⽅称为:服务器
4)关于UDP的端口绑定
⼀般情况下,服务器端,需要绑定端⼝,⽬的是为了让其他的客户端能够正确发送到此进程
客户端⼀般不需要绑定,⽽是让操作系统随机分配,这样就不会因为需要绑定的端⼝被占⽤⽽导致程序⽆法运⾏的情况
3.3 UDP服务端&客户端
1)服务端
from socket import *ip_port= ('127.0.0.1', 9999)
buffer_size= 1024udp_server= socket(AF_INET, SOCK_DGRAM) #数据报
udp_server.bind(ip_port)#udp之所以不发生粘包的现象,是因为每次发送消息都封装一个报文头信息
#recv在自己这端的缓冲区为空时,阻塞#recvfrom在自己这端的缓冲区为空时,就收一个空?(此处描述的不准确)
#udp在发送一个空时,发送的不仅仅只是一个空,而是一个带了报文头信息的空#所以在接受时,都接受了一个带报文头信息的空
whileTrue:
data, addr= udp_server.recvfrom(buffer_size) #recvfrom接受信息的格式为 (b'消息内容', ('ip地址',端口))
print(data)
udp_server.sendto(data.upper(), addr)
2)客户端
from socket import *ip_port= ('127.0.0.1', 9999)
buffer_size= 1024udp_client=socket(AF_INET, SOCK_DGRAM)whileTrue:
msg= input('>>>').strip()
udp_client.sendto(msg.encode('utf-8'), ip_port) #sendto每次发送时都要指定ip和端口,作为第二个参数
data, addr=udp_client.recvfrom(buffer_size)print(data.decode('utf-8'))
4. 粘包现象
注意:结果的编码是以当前所在的系统为准的,如果是windows,那么读出的信息就是GBK编码的,在接收端要用GBK解码。且只能从管道里读一次结果。
3.1 粘包的产生
在发送和接收数据的过程中,发送端可以1K 、1K的发送数据,而接收端的应用程序可以2K、2K或者一次3K、6K等等的取走数据;
应用程序所看到的数据是一个整体(或者是一个流stream),一条消息有多少字节对于应用程序是不可见的,因为TCP是面向流的协议,就很容易出现粘包;
而UDP段都是一条一条消息(因为每次发送消息都封装了一个报文头信息),应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,所以UDP永远不会发生粘包。
可以认为对方一次性write/send的数据为一个消息,当对方send一条信息时,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
3.2 粘包产生的原因
粘包的主要原因是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的。
发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段,若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就接收到了粘包数据。
3.3 详细分析粘包
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是 y>x 数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。(tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的)
在tcp中send和recv其实都是在操作各自的缓冲区,并不需要客户端与服务端之间一一对应;而udp中的sendto和recvfrom要一一对应,因为没有将多个数据合并
应用程序产生的数据会拷贝一份个操作系统,然后由操作系统来发送。
3.4 发生粘包的情况
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
3.5 拆包的发生情况
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
3.6 send(字节流)和recv(1024)及sendall
recv里指定的1024意思是从缓存里一次拿出1024个字节的数据。
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。
3.7 低配版解决粘包问题
分析:问题的根源在于接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
缺点:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗。
1)服务端
from socket import *
importsubprocess
ip_port= ('127.0.0.1', 8080)
back_log= 5buffer_size= 1024tcp_server=socket(AF_INET, SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)whileTrue:
conn, addr=tcp_server.accept()print('新的客户端连接', addr)whileTrue:try: #异常处理用于解决客户端意外断开时报异常的问题
cmd =conn.recv(buffer_size)if not cmd:break #解决客户端如果退出,服务端一直收空的问题
print('收到客户端的命令', cmd)#执行命令,得到命令的结果cmd_res
res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stderr= subprocess.PIPE, #将标准输入输出和错误全部扔到subprocess的管道中
stdout =subprocess.PIPE,
stdin=subprocess.PIPE
)
err=res.stdout.read()iferr:
cmd_res=errelse:
cmd_res=res.stdout.read()if not cmd_res: #若命令执行成功,但返回值为空(如cd .. 命令),我们就自己设置一个返回值发送给客户端以解决卡死的问题
cmd_res = "执行成功"
#解决粘包的问题---> 将要发送的消息的具体大小发送给客户端,告知其需要接收多少信息
length =len(cmd_res)
conn.send(str(length).encode('utf-8'))
client_ready= conn.recv(buffer_size) #中间插入了一个recv,造成中断,使得两次send的操作不会合并
if client_ready == b'ready': #如果接收到客户端发送过来的ready信号,服务端就开始发送真正的消息内容
conn.send(cmd_res)exceptException as e:print(e)breakconn.close()
2)客户端
from socket import *ip_port= ('127.0.0.1', 8080)
back_log= 5buffer_size= 1024tcp_client=socket(AF_INET, SOCK_STREAM)
tcp_client.connect(ip_port)whileTrue:
cmd= input('>>:').strip()if not cmd:continue
if cmd == "quit":breaktcp_client.send(cmd.encode('utf-8'))#解决粘包的问题---> 知道到底需要接收多少数据就可以解决
length = tcp_client.recv(buffer_size) #接收服务端发送过来的数据的长度
tcp_client.send(b'ready')
length= length.decode('utf-8')
recv_size= 0 #设定一个标识,以判断数据到底是否接收完毕
recv_msg = b''
while recv_size
recv_msg+= tcp_client.recv(buffer_size) #将每次接收到的数据合并
recv_size =len(recv_msg)print('命令的执行结果是', recv_msg.decode('gbk'))
tcp_client.close()
3.8 高配版解决粘包问题