一、基于UDP的套接字
udp是无链接的,先启动哪一端都不会报错。没有连接池backlog,不需要listen了,也不需要连接循环
UDP只有一个通信循环,进行收发。
UDP中服务端也需要绑定IP和端口(为了让给服务器发消息的能找到唯一被标识的应用程序)
udp服务端
1 ss = socket() #创建一个服务器的套接字 2 ss.bind() #绑定服务器套接字(IP地址和端口)
#这里没有listen了,listen实在TCP中挂起半链接的 3 inf_loop: #服务器无限循环 4 cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送),TCP中的send只有一个参数(发送内容);UDP中sento有两个参数(发送内荣,接收方IP和端口号) 5 ss.close() # 关闭服务器套接字
udp客户端
cs = socket() # 创建客户套接字 comm_loop: # 通讯循环 cs.sendto()/cs.recvfrom() # 对话(发送/接收) UDP中的recvfrom收两个参数组成一个元祖(接收的消息,发送方的ip和端口号) cs.close() # 关闭客户套接字
udp套接字简单示例
# 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) #addr为给本服务端发消息的客户端的IP地址和端口号 print(data) udp_server.sendto(data.upper(),addr)
#回消息给客户端,addr为相应客户端地址和端口号
# UDP客户端 from socket import * ip_port=('127.0.0.1',8080) buffer_size=1024 udp_client=socket(AF_INET,SOCK_DGRAM) #数据报:UDP协议 while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) #由于UDP中没有连接,所以每次发的时候都要指定发给哪个端口 #sento中参数表:发送的消息,服务端的IP地址和端口。 data,addr=udp_client.recvfrom(buffer_size) # print(data.decode('utf-8')) print(data)
注意:
1、如果客户端用户输入空,则成功发送给了服务端,服务端打印:b'' 并且服务端将收的空按程序写的将空变大写(还是一个空),返回给了客户端:b''。
对比TCP在发空的时候,缓冲区空就不能收发出去。即recv在缓存区中如果为空就直接阻塞住。
而UDP中空的也能被收到。recvfrom()在自己这端的缓冲区为空就接收一个空。
2、多个客户端与服务端通信时。
在我们已经实现的TCP中,服务端和客户端还不能实现并发的效果:在我们的程序当中,每次只能服务一个人,因为只有两个循环,只要外层循环不断,那一直就在内层循环中,这意味着一直在跟同一个客户端交互,直到与这个客户端断开连接后,才能与下一个客户端建立新的连接,然后重复。
而UDP实现了并发,因为UDP不用建立链接。
qq聊天(由于udp无连接,所以可以同时多个客户端去跟服务端通信)
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' import socket ip_port=('127.0.0.1',8081) udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #买手机 udp_server_sock.bind(ip_port) while True: qq_msg,addr=udp_server_sock.recvfrom(1024) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8'))) back_msg=input('回复消息: ').strip() udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' import socket BUFSIZE=1024 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ '狗哥alex':('127.0.0.1',8081), '瞎驴':('127.0.0.1',8081), '一棵树':('127.0.0.1',8081), '武大郎':('127.0.0.1',8081), } while True: qq_name=input('请选择聊天对象: ').strip() while True: msg=input('请输入消息,回车发送: ').strip() if msg == 'quit':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client_socket.recvfrom(BUFSIZE) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client_socket.close()
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' import socket BUFSIZE=1024 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ '狗哥alex':('127.0.0.1',8081), '瞎驴':('127.0.0.1',8081), '一棵树':('127.0.0.1',8081), '武大郎':('127.0.0.1',8081), } while True: qq_name=input('请选择聊天对象: ').strip() while True: msg=input('请输入消息,回车发送: ').strip() if msg == 'quit':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client_socket.recvfrom(BUFSIZE) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client_socket.close()
服务端运行结果
客户端1运行结果
客户端2运行结果
时间服务器ntp:用UDP来实现
#ntp服务端 from socket import * import time #加入时间模块 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) print(data) if not data: # 如果用户输入为空(False),前加上not。 #表示data为空就执行,表示data为空就执行, 则返回默认格式 fmt='%Y-%m-%d %X' #年-月-日 时:分:秒 else: fmt=data.decode('utf-8') #若用户自己有指定格式,则将指定格式解码作为时间格式 #自定义格式:%m-%d-%Y 月-日-年 back_time=time.strftime(fmt) #设定好格式为fmt udp_server.sendto(back_time.encode('utf-8'),addr) #将字符串形式的数据编码发出去 # PS:要是想要发送的数据为数字类型,则需要先将数字转化为字符串格式之后在编码传送出去。 udp_server.close()
TCP应用:基于TCP实现远程执行命令:
1、subprocess模块
(1) 用shell(命令解释器工具)来解释你输入的命令:两个程序之间直接通信是不行的,需要介质:利用管道PIPE来实现两个程序之间的通信。
在cmd中如果直接输入:
第一步:res=subprocess.Popen('dir', shell=True),默认会在屏幕上直接打印出命令的运行结果。即此时命令直接subprocess.Popen这个模块调用自己的命令并在后台执行此命令了,并将执行的结果直接传给屏幕(另外一个程序了)这是两个程序之间的通信。
第二步:res
<subprocess.Popen object at 0x0000000000110B128>
利用res得不到结果,只能得到一个subprocess.Popen的对象
(2) 自行控制管道,stout为程序运行正确的标准输出
stout=subprocess.PIPE,将标准输出结果交给管道,此时运行得到的数据就停在管道当中了,屏幕就不会直接输出了。
res.stdout.read() 读取管道内容,读完之后管道就为空了。如果再执行一遍,得到结果为空。
stdin=subprocess.PIPE将标准输入结果交给管道
stderr=subprocess.PIPE将标准错误输出结果(运行如果出错的时候)交给管道
这三个扔进的是三个不同的管道
2、代码实现:
#服务端 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.bind(ip_port) tcp_server.listen(back_log) while True: #建立链接大循环 conn,addr=tcp_server.accept() print('新的客户端链接',addr) while True: #收到的为命令。 #服务端基于之前三次握手建立起来的链接,会在这里等待recv #此时若客户端退出, #如果不是输入quit命令终止,而是客户端直接点结束程序运行按钮来断开连接, #用try-except来解决这种非正常关闭情况下的异常处理 try: cmd=conn.recv(buffer_size) #字节格式 #当客户端选择了quit,客户端退出循环执行了close()命令将服务端的conn直接断开时, #此时会导致服务端频繁收到空,此时用if来处理 if not cmd:break print('收到客户端的命令',cmd) #执行命令,得到命令的运行结果cmd_res #用shell(命令解释器)来处理, #两个程序之间直接通信是不行的,需要介质:利用管道PIPE来实现两个程序之间的通信 #stout为程序运行正确的标准输出,stdin为程序运行正确的标准输入,stderr为错误输出结果 res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) #这三个扔进了三个不同的管道 err=res.stderr.read() if err: #先判断是否有错误 cmd_res=err else: cmd_res=res.stdout.read() #读取正确的管道内容 #发 if not cmd_res: #cmd正常执行了,但是运行结果没有返回值 #如果不手动添加返回值的话,客户端在recv处会卡住,因为自己的缓冲区中没有东西 #因此手动返回一个“执行成功”的提示信息,反馈给客户端 cmd_res='执行成功'.encode('gbk') #windows下系统默认编码为gbk conn.send(cmd_res) except Exception as e: print(e) break #通信循环(内层的while)终止,进入下一次最外层的while循环,等待建立新的连接 #conn.close()
# 客户端 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: cmd=input('>>: ').strip() if not cmd:continue #发空:如果客户端收了空,那就会陷入死循环 if cmd == 'quit':break #如果用户输入了退出命令,此时直接退出while循环 tcp_client.send(cmd.encode('utf-8')) cmd_res=tcp_client.recv(buffer_size) print('命令的执行结果是 ',cmd_res.decode('gbk')) tcp_client.close() #导致服务端conn消失了,服务端要么报异常要么一直在收空,在收发消息的死循环中
利用UDP来实现上述远程操作:
# UDP的服务端(需绑定端口) from socket import * import subprocess ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 udp_server=socket(AF_INET,SOCK_DGRAM) udp_server.bind(ip_port) while True: #收 cmd,addr=udp_server.recvfrom(buffer_size) # 执行命令,得到命令的运行结果cmd_res #由于UDP没有连接,也就不怕客户端突然断开连接,所以既不用添加try的异常处理机制,也不用添加if判断语句 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err = res.stderr.read() if err: cmd_res = err else: cmd_res = res.stdout.read() if not cmd_res: #执行结果为空,反馈一个执行成功 cmd_res='执行成功'.encode('gbk') #发 udp_server.sendto(cmd_res,addr)
# UDP的客户端 from socket import * ip_port=('192.168.12.63',8080) back_log=5 buffer_size=1024 udp_client=socket(AF_INET,SOCK_DGRAM) while True: cmd=input('>>: ').strip() if not cmd:continue if cmd == 'quit':break udp_client.sendto(cmd.encode('utf-8'),ip_port) #加上服务端端口 cmd_res,addr=udp_client.recvfrom(buffer_size) print('命令的执行结果是 ',cmd_res.decode('gbk')) #注意编解码的对应。 udp_client.close()
TCP与UDP对比:
TCP:send发消息,recv收消息。
(1)如果收消息的缓存区中数据为空,那么recv阻塞
(2)tcp基于连接通信,如果一端断开了连接,那么另外一端的连接也跟着断开,recv将不会阻塞,收到的为空
UDP: sendto发消息,recv收消息。
(1)如果收消息缓存区为空,recvfrom不会阻塞
(2)recvfrom收的数据小于sendinto发送数据,数据丢失,不可靠
(3)只有sendinto发送数据,没有recvfrom来接收数据时,数据丢失,不可靠
注意:
1、UDP协议只负责把包发出去,对方收不收根本不管。因此,UDP中的sendinto可以一个劲地发消息,UDP中客户端可以单独存在,不依赖于服务端。
而TCP是基于连接的,必须有一个服务端先运行着,客户端去跟服务端建立链接,然后依托于链接才能传递出消息,任何一方试图把链接断开都会导致对方程序的崩溃。
2、UDP中,随便注释掉任意一条客户端的sendinto,服务端都会卡住。因为服务端有几个recvfrom就要对应几个sendinto,哪怕你只是发空的sendinto(b''),也得有这样一条sendinto语句。
一个recvfrom(x)对应一个sendinto(y),收完x个字节数据后就算是完成了。因此y>x时数据就会丢失。这意味着UDP根本不会粘包,但数据会丢失,不可靠
3、TCP数据不会丢,可靠的原因是:是指的数据传输阶段,接收端收完数据之后会反馈回一个ACK给发送端。因此发送端会知道对方的确是收到了。万一对方不给ACK表示对方没收到,此时发送方可以选择从自己的缓存区中将刚刚没发成功的再发送一遍。而发送端总是在收到ACK时才会清除缓存区已经被接收的相应内容,数据是可靠的,但会粘包。
用户态发起接收recv,然后从自己的内核态中取数据,将内核态中数据拷贝给用户态数据 。
4、recv取数据的时候并不是剪切,而是一种从自己的缓存区中取数据复制出来。缓存区为内核态,用户应用程序为用户态。用户态从内核态拷贝数据到自己的应用程序当中。