主要内容:
- 1.基于TCP协议下的socket通信流程
- 2.基于UDP协议下的socket通信流程
- 3.粘包现象
1.基于TCP协议下的socket通信流程
(1)TCP和UDP的对比
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;文件传输程序。
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文(数据包),尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
TCP 和UDP下socket差异对比图
(2) TCP协议下的socket通信流程
具体的通信流程
- 先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
- 在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
- 客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
import socket sk = socket.socket() sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字 sk.listen() #监听链接 conn,addr = sk.accept() #接受客户端链接 ret = conn.recv(1024) #接收客户端信息 print(ret) #打印客户端信息 conn.send(b'hi') #向客户端发送信息 conn.close() #关闭客户端套接字 sk.close() #关闭服务器套接字(可选) tcp_server.py
import socket sk = socket.socket() # 创建客户套接字 sk.connect(('127.0.0.1',8898)) # 尝试连接服务器 sk.send(b'hello!') ret = sk.recv(1024) # 对话(发送/接收) print(ret) sk.close() # 关闭客户套接字 tcp_client.py
socket绑定IP和端口时可能出现下面的问题:
解决方法:
#加入一条socket配置,重用ip和端口 import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #在bind前加,允许地址重用 sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字 sk.listen() #监听链接 conn,addr = sk.accept() #接受客户端链接 ret = conn.recv(1024) #接收客户端信息 print(ret) #打印客户端信息 conn.send(b'hi') #向客户端发送信息 conn.close() #关闭客户端套接字 sk.close() #关闭服务器套接字(可选) 解决办法
但是如果你加上了上面的代码之后还是出现这个问题:OSError: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试。那么只能换端口了,因为你的电脑不支持端口重用
tcp 下一个服务端与多个客户端 的通信
import socket server = socket.socket() ip_port = ("192.168.15.51",8009) server.bind(ip_port) server.listen() print("请进行登录验证") conn,addr = server.accept() #可以接受消息 flag = 0 while not flag: from_client_msg = conn.recv(1024) with open("user_info012",mode = "r+",encoding = "utf-8") as f: for line in f: if line.strip() == from_client_msg.decode("utf-8"): conn.send("登录成功".encode("utf-8")) flag = 1 break else: conn.send("账号或密码错误".encode("utf-8")) conn.close() server.close()
import socket client = socket.socket() server_ip_port = ("192.168.15.51",8009) client.connect(server_ip_port) #发消息 while 1: username, password = input("请输入你的账号"),input("请输入你的密码") msg = username + "|" + password client.send(msg.encode("utf-8")) if msg == 'good|bye': break from_server_msg = client.recv(1024).decode("utf-8") print(from_server_msg) if from_server_msg =="登录成功": break client.close()
当只有一个客户端时候是可以正常通信的,但是当再起客户端时候服务端不能够收到消息,并且当第一客户端断开连接后,服务端随之关闭,并会报错.原因是:
tcp属于长连接,长连接就是一直占用着这个链接,这个连接的端口被占用了,第二个客户端过来连接的时候,他是可以连接的,但是处于一个占线的状态,就只能等着去跟服务端建立连接,除非一个客户端断开了(优雅的断开可以,如果是强制断开就会报错,因为服务端的程序还在第一个循环里面),然后就可以进行和服务端的通信了.
修改后的
import socket server = socket.socket() ip_port = ("192.168.15.51",8009) server.bind(ip_port) server.listen() print("请进行登录验证") while 1: conn,addr = server.accept() #可以接受消息 flag = 0 while not flag: from_client_msg = conn.recv(1024) with open("user_info012",mode = "r+",encoding = "utf-8") as f: for line in f: if line.strip() == from_client_msg.decode("utf-8"): conn.send("登录成功".encode("utf-8")) flag = 1 break else: conn.send("账号或密码错误".encode("utf-8")) print("准备断开连接....") conn.close() # server.close()
import socket client = socket.socket() server_ip_port = ("192.168.15.51",8009) client.connect(server_ip_port) #发消息 while 1: username, password = input("请输入你的账号"),input("请输入你的密码") msg = username + "|" + password client.send(msg.encode("utf-8")) if msg == 'good|bye': break from_server_msg = client.recv(1024).decode("utf-8") print(from_server_msg) if from_server_msg =="登录成功": break client.close()
2.基于UDP协议下的socket通信流程
3.粘包现象
3.1 缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。 write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。 TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。 read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。 这些I/O缓冲区特性可整理如下: 1.I/O缓冲区在每个TCP套接字中单独存在; 2.I/O缓冲区在创建套接字时自动生成; 3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据; 4.关闭套接字将丢失输入缓冲区中的数据。 输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取: 1.unsigned optVal; 2.int optLen = sizeof(int); 3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen); 4.printf("Buffer length: %d ", optVal); socket缓冲区解释
3.2 windows下cmd窗口调用系统指令
a.首先ctrl+r,弹出左下角的下图,输入cmd指令,确定
b.在打开的cmd窗口中输入dir(dir:查看当前文件夹下的所有文件和文件夹),你会看到下面的输出结果。
另外还有ipconfig / ipconfig-all(查看当前电脑的网络信息),windows没有ls这个指令(ls在linux下是查看当前文件夹下所有文件和文件夹的指令,和windows下的dir是类似的),那么没有这个指令就会报错.
3.3 粘包现象(两种)
(1)subprocess模块
import subprocess cmd = input('请输入指令>>>') res = subprocess.Popen( cmd, #字符串指令:'dir','ipconfig',等等 shell=True, #使用shell,就相当于使用cmd窗口 stderr=subprocess.PIPE, #标准错误输出,凡是输入错误指令,错误指令输出的报错信息就会被它拿到 stdout=subprocess.PIPE, #标准输出,正确指令的输出结果被它拿到 ) print(res.stdout.read().decode('gbk')) print(res.stderr.read().decode('gbk')) subprocess的简单使用
注意:
- 如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码且只能从管道里读一次结果,PIPE称为管道。
- subprocess的stdout.read()和stderr.read(),拿到的结果是bytes类型,所以需要转换为字符串打印出来看。
(2)粘包现象一
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包)
import socket server = socket.socket() ip_port = ("192.168.15.51",8001) server.bind(ip_port) server.listen() conn,addr = server.accept() from_client_msg1 = conn.recv(1024).decode("utf-8") from_client_msg2 = conn.recv(1024).decode("utf-8") print('msg1:',from_client_msg1) print('msg2:',from_client_msg2) conn.close() server.close() #结果 #msg1: hahahehe #msg2:
import socket client = socket.socket() server_ip_port = ("192.168.15.51",8001) client.connect(server_ip_port) client.send("haha".encode("utf-8")) client.send("hehe".encode("utf-8")) client.close()
有时间间隔后的client (此时服务端收到的结果不会粘到一起)
import socket import time client = socket.socket() server_ip_port = ("192.168.15.51",8001) client.connect(server_ip_port) client.send("haha".encode("utf-8")) time.sleep(2) client.send("hehe".encode("utf-8")) client.close() #服务端显示内容 #msg1: haha #msg2: hehe
(3)粘包现象二
接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
import socket import subprocess server = socket.socket() ip_port = ("192.168.15.51",8006) server.bind(ip_port) server.listen(3) while 1: conn,addr = server.accept() #可以接收消息 flag = 0 while not flag: from_client_cmd = conn.recv(1024).decode("utf-8") sub_obj = subprocess.Popen(from_client_cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) server_cmd_msg = sub_obj.stdout.read() server_cmd_err = sub_obj.stderr.read() conn.send(server_cmd_msg) conn.send(server_cmd_err) conn.close()
import socket client = socket.socket() server_ip_port = ("192.168.15.51",8006) client.connect(server_ip_port) while 1: cmd = input("请输入要执行的指令(输入Q退出)>>>>>") if cmd.upper() == "Q": break else: client.send(cmd.encode("utf-8")) from_server_msg =client.recv(1024).decode("gbk") print(from_server_msg) client.close()
3.4粘包现象的解决方案
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端发一个确认消息给发送端,然后发送端再发送过来后面的真实内容,接收端再来一个死循环接收完所有数据
(1)粘包解决方案一
import socket import subprocess server = socket.socket() ip_port = ("192.168.15.51",8002) server.bind(ip_port) server.listen(3) while 1: conn,addr = server.accept() flag=0 while not flag: #来自客户端的指令 from_client_cmd = conn.recv(1024).decode("utf-8") print(from_client_cmd) sub_obj = subprocess.Popen(from_client_cmd,shell=True,stderr=subprocess.PIPE,stdout=subprocess.PIPE) server_cmd_msg = sub_obj.stdout.read() server_cmd_err = sub_obj.stderr.read() msg1_len = str(len(server_cmd_msg)) print("指令返回de正确信息的长度>>>",msg1_len) msg2_len = str(len(server_cmd_err)) print("指令返回de正确信息的长度>>>", msg2_len) conn.send(msg1_len.encode("gbk")) from_client_ack = conn.recv(1024).decode("utf-8") print("客户端对是否发送消息的确认>>>",from_client_ack) if from_client_ack == 'ok': conn.send(server_cmd_msg) else: continue conn.close()
import socket client = socket.socket() server_ip_port = ip_port = ("192.168.15.51",8002) client.connect(server_ip_port) flag = 0 while not flag: cmd = input("请输入要执行的指令>>>") client.send(cmd.encode("utf-8")) from_server_msg_len = client.recv(1024).decode("gbk") print('服务端将要发送信息的长度',from_server_msg_len) client.send("ok".encode("utf-8")) from_server_stdout = client.recv(int(from_server_msg_len)).decode("gbk") print('收到的正确信息:', from_server_stdout) client.close()