UDP协议的服务端与客户端
由于UDP不像TCP一样需要握手,所以只需要绑定端口以后直接开始通信即可,不像TCP一样需要listen.# UDPserver
from socket import *
udp_server = socket(AF_INET, SOCK_DGRAM)
udp_server.bind(ip_port)
while True:
data = udp_server.recvfrom(buffer_size)
print(data)
# UDPclient
from socket import *
ip_port = ('127.0.0.1', 8080)
udp_client = socket(AF_INET, SOCK_DGRAM)
while True:
udp_client.sendto('hello'.encode('utf-8'), ip_port)
唯一需要注意的是,udp的sendto需要把ip和端口号在每次发送的时候都写上
在可以建立了简单的服务端和客户端之后,由于双方往来发送的介质就是字节流的字符串消息,所以可以针对这个消息,在客户端和服务端加入各种逻辑,就可以实现简单的功能.比如可以做一个通过远程执行命令的服务端和客户端.
这里需要用到subprocess模块
# server
from socket import *
import subprocess
ip_port = ('127.0.0.1', 8080)
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
while True:
print('server is ready.')
conn, addr = tcp_server.accept()
while True:
try:
data = conn.recv(buffer_size)
if not data:
conn.close()
break
res = subprocess.Popen(data.decode('utf-8'),
shell=True, stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE) # 这句是执行结果,将结果分别放入标准输出,标准输入和标准错误里
err = res.stderr.read() # 读取标准错误,如果标准错误不是空,则返回错误信息
if err:
res_data = err
else:
res_data = res.stdout.read()
if not res_data: # 即使标准错误为空,但是很可能执行成功后不返回结果,此时再加一层判断,如果还是为空,说明命令成功执行,也返回一段东西,不会导致客户端卡住
conn.send('Command sucessfully executed'.encode('gbk'))
else:
conn.send(res_data) # 注意,系统执行的结果虽然是字节,但是以系统默认的gbk编码,所以在接收端需要改成编码为gbk
print('向客户端传送的消息是: ', res_data.decode('gbk'))
except Exception:
conn.close()
break
客户端基本没变化,主要是在显示的时候,需要采用gbk进行解码.
from socket import *
ip_port = ('127.0.0.1', 8080)
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
if msg=='quit':
break
tcp_client.send(msg.encode('utf-8'))
print('向服务端发送了:',msg)
data = tcp_client.recv(buffer_size)
print('客户端收到的是: ',data.decode('gbk'),sep='\n')
tcp_client.close()
粘包
用上边的程序执行了一下ipconfig/all 结果发现没有收完,又运行了一下dir,结果发现收到了ipconfig/all的数据,再dir,发现又乱了. 这是因为缓冲区内的数据太多,在第一次取的时候没有取完,下次再用recv的时候又接着取了1024字节的数据.看来需要用一种方法,让客户端来取数据的时候,收到符合要求指定长度的数据. 由于TCP是保证字节流的传输,而这些字节如何分割,是应用程序的事,如果不对消息的边界做处理,则应用程序无法正常通信.这种情况叫做[粘包](https://wiesen.github.io/post/tcp%E6%96%AD%E5%8C%85%E7%B2%98%E5%8C%85%E9%97%AE%E9%A2%98/). 只有TCP协议有粘包现象,UDP协议没有粘包现象.UDP不基于连接,所以可以任意发,如果缓冲区不够,收的少,剩下的部分就全部都丢失了.通过UDP可以测试到,如果没收完,后边的数据直接就丢弃了,但TCP就会粘在一起.
原因是:
应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
做一个简单的测试:
# server
from socket import *
ip_port = ('127.0.0.1', 8080)
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
conn,addr = tcp_server.accept()
data = conn.recv(buffer_size)
print('first data: ', data.decode('utf-8'))
data = conn.recv(buffer_size)
print('2nd data: ', data.decode('utf-8'))
data = conn.recv(buffer_size)
print('3rd data: ', data.decode('utf-8'))
# client
from socket import *
ip_port = ('127.0.0.1', 8080)
back_log = 5
buffer_size = 1024
tcp_client = socket(AF_INET, SOCK_STREAM)
tcp_client.connect(ip_port)
tcp_client.send('hello'.encode('utf-8'))
tcp_client.send('hell0'.encode('utf-8'))
tcp_client.send('hellO'.encode('utf-8'))
tcp_client.close()
可以看到虽然客户端发送了三次,但是服务端第一次就已经收到了全部的消息.这是收取的比数据量大发生的粘包.
如果将每次的buffer_size改成5,即每次取5个,则发现可以得到了正确的消息.收取的比数据量小,一样可以发生粘包.只要理解了收发缓冲区的概念,那么就可以来尝试解决粘包的问题:即要想办法确认数据有多长.
因为粘包是发送的时候粘在一起,可以想到,在发送之前,先向另外一端发一个长度,对方收到长度之后返回一个标记,之后再传输数据,然后再用一个循环接受所有数据直到长度到达之前传送的长度.用一个比较简单的版本可以解决粘包问题,那就是每个通信循环对于服务端来说是1收1发,只要额外增加一个发送长度的环节和收到确认的环节,改成2收2发即可,客户端则对应修改成2发2收.
# server
from socket import *
import subprocess
ip_port = ('127.0.0.1', 8080)
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
while True:
print('server is ready.')
conn, addr = tcp_server.accept()
while True:
try:
data = conn.recv(buffer_size) # 第一次接收,接收指令
if not data:
conn.close()
break
res = subprocess.Popen(data.decode('utf-8'),
shell=True, stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
err = res.stderr.read()
if err:
res_data = err
else:
res_data = res.stdout.read()
if not res_data:
res_data = 'Command sucessfully executed'.encode('gbk')
data_length = str(len(res_data)) # 获得要发送的数据包长度
conn.send(data_length.encode('utf-8')) # 第一次发送,先把长度发掉
client_ready = conn.recv(buffer_size) # 第二次接收,接收客户端返回的准备信息,说明客户端接受到了长度
if client_ready.decode('utf-8') == ""ready"": # 第二次发送,判断客户端准备好之后,发送信息
conn.send(res_data)
except Exception:
conn.close()
break
# client
from socket import *
ip_port = ('127.0.0.1', 8080)
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
if msg == 'quit':
break
tcp_client.send(msg.encode('utf-8')) # 第一次发送,发送指令
length = int(tcp_client.recv(buffer_size).decode('utf-8')) # 第一次接收,获得长度
tcp_client.send(b'ready') # 第二次发送,发送就绪消息
recv_size = 0
recv_msg = b''
while recv_size < length: # 第二次接收,用一个循环不断接收消息后拼接消息,直到长度OK,然后一次循环完成,等待下一个通信循环
cmd_res = tcp_client.recv(buffer_size)
recv_msg += cmd_res
recv_size += len(cmd_res)
print('客户端消息长度:{},收到的是:\n{}'.format(length, recv_msg.decode('gbk')))
tcp_client.close()
再来分析一下看看,可以知道,实际上就是在每次传输数据之前,传输了一部分额外的数据用于确定长度.如果客户端和服务端都是自己写的,那完全可以减少发送的次数,只要约定好,发送数据的前一个固定的部分是表示长度,或者还可以附带其他信息,也就无所谓粘包了,对于另一端来讲,只需要先解析头部数据,再按照长度拿后边的数据就可以了.这其实,就是在写自己的软件内部的通信协议.固定的字节数表示一个范围内的数字,可以用struct来完成.而文件名称和其他信息,可以用pickle来转换成字节,这样就构成了 报头长度--报头本身--数据流 的形式,解析的时候先解析固定长度的报头长度,得到报头本身的长度,按照这个长度解析报头,然后开始传输数据即可.具体代码就不写了,因为通过struct来固定长度不是一个很好的做法,看看以后会有什么做法.这里关键是本质,就是想办法让另外一端了解长度.