基于tcp的套接字
文章目录
一、套接字的工作流程
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
二、基于tcp的套接字通信
基于上面的套接字工作原理,我们可以用python编写处如下的一段代码:
python
#服务端
import socket
#socket.AF_INET表示套接字,socket.SOCK_STREAM表示tcp,tcp也称为流式协议
#创建套接字对象
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#绑定服务端ip和端口
phone.bind(('127.0.0.1',8081))
#开始监听,listen表示半连接池,限制的是请求数
phone.listen(5)
# 连接循环,服务端需要一直开启等待客户端的连接(连接循环)
while True:
#收到客户端的请求,通过三次握手与四次挥手建立通信通道
#conn是建立的通信通道,client_addr是客户端的信息
#当没有建立链接请求时,服务端会一直停在phone.accept()处
conn,client_addr=phone.accept()
#通信通道建立完成,与客户端持续通信(通信循环)
while True:
try:
print('服务端正在收数据...')
#为了降低内存的压力,需要限制每次接收的字节数
#当没有接收到客户端的消息时,服务端会一直停在conn.recv(1024)处
data=conn.recv(1024)
#linux中客户端中断后服务端会接收空字符,此时需要跳出通信循环
if len(data) == 0:break
print('来自客户端的数据',data)
#回复客户端的信息
conn.send(data.upper())
#windows中客户端连接中断会报错,需要用try推出通信循环
except ConnectionResetError:
break
#关闭通信通道,服务端准备与下一个客户端建立通信链接
conn.close()
#关闭套接字对象
phone.close()
python
#客户端
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#客户端不需要绑定ip和端口,只需向服务端的ip和端口发送请求
phone.connect(('127.0.0.1',8080)) # 指定服务端ip和端口
#通信循环
while True:
msg=input('>>: ').strip()
#套接字中无法发送空字符
if len(msg) == 0:continue
phone.send(msg.encode('utf-8'))
data=phone.recv(1024)
print(data)
phone.close()
如果在重启服务端的过程中出现如下的情况表示服务端仍在四次挥手的time_wait状态(服务端进程依然在后台运行),此时可以采取两种方法。
- 修改绑定给服务端的端口号
- 在绑定服务端的ip和端口前加上phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
三、基于udp的套接字通信
基于udp协议编写的套接字如下:
python
#服务端
import socket
#socket.SOCK_DGRAM表示udp协议,udp是数据报协议
server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(('127.0.0.1',8080))
#udp协议不需要建立通信通道,因此它是不可靠的通信协议
#简单来说tcp是一对一的收发消息,一个客户端结束才会回应其他客户端
#udp是一对多的收发消息,由客户端发送消息时服务端就会回应
while True:
#接收客户端的消息
data,client_addr=server.recvfrom(1024)
print('===>',data,client_addr)
#发送消息给客户端,由于没有链接通道,发送信息需要带上客户端的ip和端口信息
server.sendto(data.upper(),client_addr)
server.close()
python
#客户端
import socket
client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
msg=input('>>: ').strip()
#向服务端的ip和端口发送信息
client.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
data,server_addr=client.recvfrom(1024)
print(data)
client.close()
四、粘包现象
将服务端的代码作如下的修改:
python
import socket,subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while True:
conn,client_addr=phone.accept()
while True:
try:
data=conn.recv(1024)
if len(data) == 0: break
a=subprocess.Popen(data.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
res=a.stdout.read()
conn.send(res)
except ConnectionResetError:
break
conn.close()
phone.close()
我们尝试在客户端通过指令tasklist查看服务端的进程列表,第一次客户端向服务端发送tasklist命令返回如下的结果:
映像名称 PID 会话名 会话# 内存使用
========================= ======== ================ =========== ============
System Idle Process 0 Services 0 8 K
System 4 Services 0 12 K
Registry 296 Services 0 26,600 K
smss.exe 892 Services 0 528 K
csrss.exe 1124 Services 0 2,840 K
wininit.exe 1236 Services 0 3,400 K
services.exe 1308 Services 0 8,912 K
lsass.exe 1332 Services 0 20,172 K
svchost.exe 1460 Services 0 29,432 K
fontdrvhost.exe 1484 Services 0 104 K
WUDFHost.exe 1536 Services 0 2,952 K
svchost.
第二次当客户端向服务端发送ping www.baidu.com时会发现返回的结果依然是客户端的进程列表:
exe 1596 Services 0 15,092 K
svchost.exe 1640 Services 0 6,416 K
WUDFHost.exe 1764 Services 0 21,224 K
svchost.exe 1876 Services 0 3,868 K
svchost.exe 1884 Services 0 7,436 K
svchost.exe 1904 Services 0 4,420 K
svchost.exe 1940 Services 0 9,876 K
svchost.exe 1948 Services 0 7,880 K
svchost.exe 2036 Services 0 7,128 K
svchost.exe 1304 Services 0 15,372 K
svchost.exe 2128 Services 0 4,932 K
svchost.exe 2140 Services 0 6,348 K
svchost.exe 2148 Services 0 7,032 K
svchost.exe
这是怎么回事呢?我们知道tcp协议是流式协议,也就是说基于tcp协议发送消息时,服务端套接字会把需要发送的消息给自己的操作系统,而自己的操作系统将这些消息一段一段发送给客户端的操作系统,由于是一段一段的发送,客户端无法判断一条消息的始末,所以客户端套接字每次只从操作系统中取字节数限制字节的消息,当发送的消息量过大时,只有一部分消息会被接收并打印到终端上,剩余的消息依然在客户端的操作系统中。当我们再次向服务端发送消息接收消息以后,套接字会先接收上次没有接受完的消息,再接受新的消息,这就产生了粘包现象。
另外如果tcp多次短间隔的发送消息,发送端的套接字会将这些消息并再一起发送,这样会发送接受方的另一种粘包问题。
这时候肯定有人要说如果我们不限制套接字每次接受的字节数是不是就能解决这个问题呢?问题是如果我们接受的是一个很大的内容,比如50g,套接字会将接受的消息全部读入内存,这就会引发内存爆满的情况,显然这种解决方式是不可取的。
udp协议是数据报式的协议,也就是说udp每次收发消息都是以一个数据报为单位的(套接字会给每次的消息加上消息头),每次接受消息都会一次取完。如果服务端接收的字节限制比接收内容小时,多出来的内容会丢失(windows中会报错),而不会发送粘包的问题。由于udp的消息都含有消息头,所以即便是短时间内发送多次消息,也不会发生上面说到的第二种粘包问题。
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。