一、Socket相关知识
1、socket是什么:
socket是应用层与TCP/IP协议族通信的中间软件抽象层,他是一组接口。在设计模式中,Socket其实就是一个门面模式。
它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入了解tcp/udp协议,Socket已经为我们封装好了,我们只需要遵循Socket的规定去编程,写出程序自然就是遵循tcp/udp标准的。
2、看一张图片帮助理解
3、tcp套接字工作流程
(1)、工作流程图
(2)、python的Socket()模块函数用法
1 from socket import * 2 3 socket(socket_family,socket_type,protocal=0) 4 socket_family 可以是 AF_UNIX或者AF_INET。socket_type可以是SOCK_STREAM或者SOCK_DGRAM。protocal 一般不填,默认值为0 5 6 获取tcp/ip套接字 7 tcpSock = socket(socket.AF_INET, socket.SOCK_STREAM) 8 9 获取udp/ip套接字 10 udpSock = socket(socket.AF_INET, socket.SOCK_DGRAM) 11 12 #############其他套接字函数 13 s=socket(socket_family,socket_type,protocal=0) 14 服务端套接字函数 15 16 s.bind() 绑定(主机,端口)到套接字 17 s.listen() 开始监听 18 s.accept() 被动接受TCP客户端的连接,等待连接的到来 19 20 客户端套接字函数 21 s.connect() 主动初始化tcp服务器连接 22 s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 23 24 公用的套接字函数 25 s.recv() 接收TCP数据 26 s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完) 27 s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) 28 s.recvfrom() 接收UDP数据 29 s.sendto() 发送UDP数据 30 s.getpeername() 连接到当前套接字的远端的地址 31 s.getsockname() 当前套接字的地址 32 s.getsockopt() 返回指定套接字的参数 33 s.setsockopt() 设置指定套接字的参数 34 s.close() 关闭套接字 35 36 面向锁的套接字方法 37 s.setblocking() 设置套接字的阻塞与非阻塞模式 38 s.settimeout() 设置阻塞套接字操作的超时时间 39 s.gettimeout() 得到阻塞套接字操作的超时时间 40 41 面向文件的套接字函数 42 s.fileno() 套接字的文件描述符 43 s.makefile() 创建一个与该套接字相关的文件。
(3)、基于tcp的Socket模块例子
1 from socket import * 2 3 srv_msg = ("127.0.0.1",8000) 4 bufsize = 1024 5 6 sock_server = socket(AF_INET,SOCK_STREAM) 7 sock_server.bind((srv_msg)) 8 sock_server.listen(5) 9 10 print("服务端启动") 11 while True: 12 conn, addr = sock_server.accept() 13 14 while True: 15 try: 16 data=conn.recv(bufsize) 17 print("服务端收到了一条消息: %s " % data.decode("utf-8")) 18 data = "服务端说:%s" % data.decode("utf-8") 19 conn.send(data.encode("utf-8")) 20 except Exception as e: 21 break 22 conn.close() 23 sock_server.close()
服务端
1 from socket import * 2 3 srv_msg = ("127.0.0.1",8000) 4 bufsize = 1024 5 6 socket_client = socket(AF_INET,SOCK_STREAM) 7 socket_client.connect(srv_msg) 8 9 while True: 10 msg_input = input(">>") 11 if not msg_input:continue # 防止输入为空而卡住死机,为什么会卡住?请看后面详解 12 socket_client.send(msg_input.encode("utf-8")) 13 data=socket_client.recv(bufsize) 14 print(data.decode("utf-8")) 15 16 socket_client.close()
客户端
socket收发消息的原理图
当client端直接回车发送了一个空,在自己的缓冲区是一个空。经过网络传输,到server端的时候,服务端写入缓冲区的也是空,。
那么服务端就一直检测自己的缓冲区是否有数据,所以就在等待接受数据。数据没有收到,自然也就无法正常在自己的缓冲区写入回复消息。
这样也就无法返回给客户端消息。所以客户端就会一直等待服务端的回应。
(4)、解决socket服务端断开后出现端口占用的情况
(4.1)、链路复用
1 #加入一条socket配置,重用ip和端口 2 3 phone=socket(AF_INET,SOCK_STREAM) 4 phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 5 phone.bind((‘127.0.0.1‘,8080))
(4.2)、linux内核参数优化
1 发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决, 2 vi /etc/sysctl.conf 3 4 编辑文件,加入以下内容: 5 net.ipv4.tcp_syncookies = 1 6 net.ipv4.tcp_tw_reuse = 1 7 net.ipv4.tcp_tw_recycle = 1 8 net.ipv4.tcp_fin_timeout = 30 9 10 然后执行 /sbin/sysctl -p 让参数生效。 11 12 net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; 13 14 net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; 15 16 net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 17 18 net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间 19
(5)、基于udp的套接字例子
from socket import * import time ip_port = ("127.0.0.1",8080) buf_size = 1024 udp_srv = socket(AF_INET,SOCK_DGRAM) udp_srv.bind(ip_port) print("udp服务端启动") while True: data,addr=udp_srv.recvfrom(buf_size) if not data: fmt = "%Y-%m-%d %X" else: fmt = data.decode(‘utf-8‘) back_msg = time.strftime(fmt) udp_srv.sendto(back_msg.encode("utf-8"),addr) #这里和tcp区别比较大 udp_srv.close()
服务端
from socket import * buf_size = 1024 ip_port = ("127.0.0.1",8080) udp_client = socket(AF_INET,SOCK_DGRAM) while True: msg = input(">>") udp_client.sendto(msg.encode("utf-8"),ip_port) data = udp_client.recv(buf_size) print(data.decode("utf-8")) udp_client.close()
客户端
4、利用socket编写远程执行命令的程序
(1)、第一版本程序
1 from socket import * 2 import subprocess 3 4 ip_port = ("127.0.0.1",8000) 5 back_log = 5 6 buf_size = 1024 7 8 socket_server = socket(AF_INET,SOCK_STREAM) 9 socket_server.bind(ip_port) 10 socket_server.listen(back_log) 11 12 print("服务端启动") 13 while True: 14 conn, addr = socket_server.accept() 15 print("客户端 %s 连上服务器!" % addr[0]) 16 17 while True: 18 try: 19 cmd = conn.recv(buf_size) 20 if not cmd:break 21 print("收到客户端 %s 命令" % cmd.decode("utf-8")) 22 res = subprocess.Popen(cmd.decode("utf-8"),shell=True, 23 stdin=subprocess.PIPE, 24 stdout=subprocess.PIPE, 25 stderr=subprocess.PIPE) 26 err = res.stderr.read() 27 if err: 28 res_msg = err 29 else: 30 res_msg = res.stdout.read() 31 if not res_msg: 32 res_msg = "执行成功".encode("gbk") 33 conn.send(res_msg) 34 except Exception as e: 35 print(e) 36 break 37 # conn.send() 38 conn.close() 39 socket_server.close()
服务端
1 from socket import * 2 3 ip_port = ("127.0.0.1",8000) 4 back_log = 5 5 buf_size = 1024 6 7 socket_client = socket(AF_INET,SOCK_STREAM) 8 socket_client.connect(ip_port) 9 10 while True: 11 cmd = input(">>:") 12 if not cmd:continue 13 if cmd == "quit" or cmd == "exit":break 14 socket_client.send(cmd.encode("utf-8")) 15 res = socket_client.recv(buf_size) 16 print("命令的结果是:",res.decode(‘gbk‘)) 17 socket_client.close()
客户端
第一版本会出现粘包现象。
为什么会粘包,简单来说就是接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
粘包显现只有tcp会出现,udp不会出现。
粘包现象有两种体现方式:
(1.1)、发送端数据很小,时间间隔很短这个时候发送端的优化算法就会将数据合并到一起发送。
(1.2)、发送方数据量很大,接收方没有及时接收缓冲区的包,造成多个包接收。
许海峰老师的说法(摘自他的博客)
http://www.cnblogs.com/linhaifeng/articles/6129246.html#_label5
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
- 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时才会清除缓冲区内容。数据是可靠的,但是会粘包。
(2)、第二版本程序
1 from socket import * 2 import subprocess 3 4 ip_port = ("127.0.0.1",8000) 5 back_log = 5 6 buf_size = 1024 7 8 sck_server = socket(AF_INET,SOCK_STREAM) 9 sck_server.bind(ip_port) 10 sck_server.listen(back_log) 11 12 print("服务端启动") 13 while True: 14 conn,addr = sck_server.accept() 15 16 while True: 17 try: 18 data = conn.recv(buf_size) 19 print("服务端收到:%s" % data.decode("utf-8")) 20 if not data:break 21 sub = subprocess.Popen(data.decode(‘utf-8‘),shell=True, 22 stdout=subprocess.PIPE, 23 stderr=subprocess.PIPE) 24 res_msg = sub.stderr.read() 25 if res_msg: #当命令错误管道有信息,则表明命令出错 26 res = res_msg 27 else: #其他情况就正常返回命令的结果 28 res = sub.stdout.read() 29 30 if not res: # 这里是为了兼容异常命令,例如exit,windows和linux都有这个命令是退出系统的,所以他的返回值为空,如果不封装信息返回给客户端,客户端会卡死。 31 res = "执行成功".encode("gbk") # windows默认编码是gbk 32 33 res_length = len(res) # 将数据的长度保存 34 conn.send(str(res_length).encode("utf-8")) # 将数据的长度传给客户端 35 data = conn.recv(buf_size).decode("utf-8") # 接收客户端收到数据长度后返回来的口令。 36 if data == "go": #收到可以传正式数据的口令后开始传数据 37 conn.sendall(res) 38 except Exception as e: 39 print(e) 40 break 41 conn.close() 42 sck_server.close()
服务端
1 from socket import * 2 3 ip_port = ("127.0.0.1",8000) 4 back_log = 5 5 buf_size = 1024 6 7 sck_client = socket(AF_INET,SOCK_STREAM) 8 sck_client.connect(ip_port) 9 10 while True: 11 msg = input(">>: ") 12 if not msg:continue 13 if msg == "exit":break 14 15 sck_client.send(msg.encode("utf-8")) 16 data = sck_client.recv(buf_size) 17 data_length = int(data.decode("utf-8")) 18 sck_client.send("go".encode("utf-8")) 19 recv_size = 0 #初始化收到的数据的字节数 20 data_new = b"" #初始化数据 21 while recv_size < data_length: 22 print(recv_size) 23 data_new += sck_client.recv(buf_size) #拼接收到的数据 24 recv_size+=len(data_new) #将收到的数据数量合并 25 26 print("客户端收到服务端的返回信息:%s" % data_new.decode("gbk")) 27 sck_client.close()
客户端
第二版本的思路就是先将服务端所要传给客户端的数据大小先发给客户端,这样客户端就根据服务端传过来的大小来接收数据。
(3)、第三版程序
1 from socket import * 2 import subprocess,struct,json 3 4 ip_port = ("127.0.0.1",8000) 5 back_log = 5 6 buf_size = 1024 7 8 sck_server = socket(AF_INET,SOCK_STREAM) 9 sck_server.bind(ip_port) 10 sck_server.listen(back_log) 11 12 print("服务端启动") 13 while True: 14 conn,addr = sck_server.accept() 15 16 while True: 17 try: 18 data = conn.recv(buf_size) 19 print("服务端收到:%s" % data.decode("utf-8")) 20 if not data:break 21 sub = subprocess.Popen(data.decode(‘utf-8‘),shell=True, 22 stdout=subprocess.PIPE, 23 stderr=subprocess.PIPE) 24 res_msg = sub.stderr.read() 25 if res_msg: #当命令错误管道有信息,则表明命令出错 26 res = res_msg 27 else: #其他情况就正常返回命令的结果 28 res = sub.stdout.read() 29 30 if not res: # 这里是为了兼容异常命令,例如exit,windows和linux都有这个命令是退出系统的,所以他的返回值为空,如果不封装信息返回给客户端,客户端会卡死。 31 res = "执行成功".encode("gbk") # windows默认编码是gbk 32 33 headers = {"data_size":len(res)} 34 head_json = json.dumps(headers) 35 head_json_bytes = bytes(head_json,encoding="utf-8") 36 37 conn.send(struct.pack("i",len(head_json_bytes))) 38 conn.send(head_json_bytes) 39 conn.sendall(res) 40 41 except Exception as e: 42 print(e) 43 break 44 conn.close() 45 sck_server.close()
服务端
1 from socket import * 2 import struct,json 3 4 ip_port = ("127.0.0.1",8000) 5 back_log = 5 6 buf_size = 1024 7 8 sck_client = socket(AF_INET,SOCK_STREAM) 9 sck_client.connect(ip_port) 10 11 while True: 12 msg = input(">>: ") 13 if not msg:continue 14 if msg == "exit":break 15 sck_client.send(msg.encode("utf-8")) 16 17 head = sck_client.recv(4) 18 head_json_len = struct.unpack("i",head)[0] 19 head_json = json.loads(sck_client.recv(head_json_len).decode("utf-8")) 20 21 data_length = head_json[‘data_size‘] 22 recv_size = 0 #初始化收到的数据的字节数 23 data_new = b"" #初始化数据 24 while recv_size < data_length: 25 print(recv_size) 26 data_new += sck_client.recv(buf_size) #拼接收到的数据 27 recv_size+=len(data_new) #将收到的数据数量合并 28 29 print("客户端收到服务端的返回信息:%s" % data_new.decode("gbk")) 30 sck_client.close()
客户端
解决思路:
服务端:
将包头做成字典,字典里面包含要发送的真实数据的详细信息,然后通过json进行序列化,然后用struck将序列化后的数据长度打包成4个字节
发送时,先讲包头长度发送,再讲编码包头内容发送,最后进行真实数据的发送
客户端:
先收包头长度,用struct取出来,根据取出的长度收取包头内容,然后解码,反序列化,从反序列化的结果中取出要收取数据的详细信息。最后取真实数据,进行拼接和长度判断。