Python3 socket编程
1.C/S 架构和socket的关系
- socket就是为了完成C/S架构的开发
- 用socket来做一个服务端(客户端)分别运行在不同的机器上
2.OSI七层协议
- 应用层 ==> 提供应用软件的接口,以设置于另以软件之间的通信(有http、https、ftp、ssh等协议)
- 表达层 ==> 把数据转换为能与接收者系统兼容的传输格式
- 会话层 ==> 负责在数据传输中设置和维护计算机网络中两台计算机之间的通信
- 传输层 ==> 把传输表头加上数据形式成数据包,包括了所有使用协议等发送信息,提供一个端口
- 网络层 ==> 决定数据的路径选择和转寄,将网络表头加至数据包,以形成分组
- 数据链路层 ==> 负责网络的寻址、错误侦测和改错;当表头和表尾被加至数据包时会形成帧
- 物理层 ==> 负责计算机通信设备和网络媒体之间的互通
3.socket是什么
socket是在应用层与TCP/IP协议族通信的中间软件抽象层;本质就是一个接口,把复杂的TCP/IP协议隐藏在socket接口后面。
4.套接字
套接字课分为文件套接字和网络套接字:
- 文件套接字:一台机器上不同程序之间的通信都是基于底层文件系统(AF_UNIX)
- 网络套接字:还是两个程序进行通信,但依托的媒介是网络(AF_INET)
5.套接字的工作流程
6.socket基本用法
服务端:
import socket host = '127.0.0.1' port = 8080 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建一个基于网络通信的TCP协议的socket对象 server.bind((host, port)) server.listen(5) # 5表示最大的同时连接数 conn,addr = server.accept() # conn表示链接;addr表示地址;返回的结果是一个元组 msg = conn.recv(1024) # 接受信息,1024表示接收1024个字节的信息 print("客户端发来的消息是:%s" %msg.decode('utf-8')) conn.send(msg.upper()) # 发送的消息 # 断开链接 conn.close() server.close()
客户端:
import socket host = '127.0.0.1' port = 8080 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((host, port)) msg = 'hello' #特别注意这里的send方式,在课堂中练习的时候,不管是客户端发送还是服务端发送,都要发送bytes的格式,而这里的代码只要客户端发送的时候encode utf8转换字节流即可,服务端想要查看使用decode解码即可,如果服务端不需要使用正规格式查看,直接返回给客户端就行,所以验证那句话,只要想要发送,格式一定是bytes的 #这里msg.encode('utf-8')就是把格式转成bytes格式,不信可以自己打印类型看看 #“encode(编码):按照某种规则将“文本”转换为“字节流”。比如课堂上使用的转换方式方便很多 client.send(msg.encode('utf-8')) data = client.recv(1024) print("服务的发来的消息:%s" %data) client.close()
这里还是贴出课堂上那种另类而又麻烦的转换方式,这里对比一下
服务端:
import socket,subprocess ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.bind(ip_port) sk.listen(5) while True: print ('server waiting...') conn,addr = sk.accept() while True: #接收来自客户端的命令并打印出来(不管是发送还是接收的都是bytes格式) client_data = conn.recv(1024) print ("client_cmd:",str(client_data,'utf8')) #如果客户端没有数据,直接跳出这次循环,一般是客户端断开后防止死循环 if not client_data:break; #将客户端的命令转成成为str用subprocess执行 cmd = str(client_data,'utf8').strip() cmd_call = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE) cmd_result = cmd_call.stdout.read() #如果客户端返回的结果==0,那么代表执行的就是如cd,或者命令执行出错了,那么就返回我们自定义的提示 if len(cmd_result) == 0: cmd_result = b'cmd executio has no output' #(不管是发送还是接收的都是bytes格式) #发送回客户端也要转成bytes,可以看到只要是发送就要转成bytes ack_msg = b"CMD_RESULT_SIZE|%s" %len(cmd_result) conn.send(ack_msg) conn.send(cmd_result) conn.close()
客户端:
import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.connect(ip_port) while True: user_input = input("cmd>>:").strip() if len(user_input) == 0:continue if user_input == 'q':break #这里的(bytes(user_input,'utf-8'))等价于msg.encode('utf-8') #每次发送一定要转成bytes sk.send(bytes(user_input,'utf-8')) server_reply = sk.recv(1024) print ("server_reply:",str(server_reply,'utf8')) sk.close()
本篇文章后面每次发送转换都使用encode('utf-8')
7.socket底层原理
我们知道在TCP协议下如果client端和server端建立连接的话需要经历三次握手:
- 如果client想要建立连接到server端的连接,client会向server发送一个SYN请求
- 在server端收到SYN请求后会返回一个ACK,在原SYN值的基础上加上1,表示同意建立连接;并且同时还会向client发送一个SYN请求
- 在client端接收到server发送过来的SYN请求后也会返回一个ACK,在该SYN的基础上加上1。至此client端和server端的连接建立完成
现在我们对应到socket编程上来会发现这三次握手是在 accept() 这一步上建立的
这里还需要补充概念叫的一个是Tcp SYN flood(TCP洪水攻击),其原理是在客户端向服务端发送连接请求并且服务端同意客户端进行连接后客户端并没有向服务端发送ACK;这时如果黑客使用1万个客户端向服务端发送请求并不发送最后的确认ACK包的话会严重影响到服务器的内存和带宽;这时又引出一个backlog(连接队列)参数,服务器会将处于半连接的TCP连接放入连接队列中,所以backlog参数对应到socket编程上来就是listen()的参数。
连接建立后的数据传输就相对简单许多了,客户端向服务器发送一条数据,服务端接收到该数据后会向客户端返回一个ACK包,表示已经就收到该数据,这就是为什么我们称为TCP为可靠传输协议。
数据的传输对应到socket编程上的话分别是recv() 和 send()两步。
当数据传输完成后会进行四次挥手进行连接的断开;如果客户端的数据先发完的话客户端就会向服务端发送FIN包,请求断开客户端到服务端的连接,这是服务端或返回一个ACK包,同意断开客户端到服务端的连接;接下去到服务端发送完数据后也会向客户端发送一个FIN包请求断开服务端到客户端的连接,这是客户端或返回一个ACK包,同意断开服务端到客户端的连接;至此服务端到客户端的连接和客户端到服务端的连接均已断开。
那么为什么建立连接只需要3次握手,而断开连接需要4次挥手呢?这是因为断开连接的前提是数据发送的完成,如果将服务端的ACK包和FIN包同时发送的话就无法确保两端的数据传输均已完成。
8.socket编程的收发消息原理
服务端:
from socket import * ipaddr = '127.0.0.1' port = 8000 back_log = 5 tcp_server = socket(AF_INET, SOCK_STREAM) tcp_server.bind((ipaddr, port)) tcp_server.listen(back_log) while True: conn,addr = tcp_server.accept() while True: try: data = conn.recv(1024) print("data is %s" %data.decode('utf-8')) conn.send(data.upper()) except Exception: break conn.close() tcp_server.close()
客户端:
from socket import * ipaddr = '127.0.0.1' port = 8000 tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.connect((ipaddr, port)) while True: msg = input(">>>: ") tcp_client.send(msg.encode("utf-8")) data = tcp_client.recv(1024) print("data is %s" %data.decode("utf-8")) tcp_client.close()
这时我们如果在客户端运行程序,在输入的时候直接回车,即输入一个空值,这时候你就会发现客户端和服务端都卡在这里,服务端没有收到消息,客户端也没有收到消息;这是因为程序收发都是通过内核态来进行的;当程序(程序是在用户态中)通过系统能够调用内核态中的相应资源时才会将内容发聩给用户,即接收成功;同理当程序发送消息给其他机器时也是通过系统的处理来交给内核态通过网卡进行发送的。在上面的例子中空值是无法通过网络进行传输的,所以服务端的内核态中的资源为空,这就会导致服务端上的程序无法在内核态中找到相应的内容,程序当然也就无法进行下去。
注:在Linux下当客户端断开和服务端的连接的时候,服务端会一直接收一个空值,从而会陷入一个死循环中;这时我们需要使用一个if判断来结束该循环。如:
#!/usr/bin/python3 #-*- conding: utf-8- from socket import * ipaddr = '192.168.16.148' port = 8000 back_log = 5 tcp_server = socket(AF_INET, SOCK_STREAM) tcp_server.bind((ipaddr, port)) tcp_server.listen(back_log) while True: conn,addr = tcp_server.accept() while True: data = conn.recv(1024) if not data: break print("data is %s" %data.decode('utf-8')) conn.send(data.upper()) conn.close() tcp_server.close()
9.基于UDP的Socket编程
服务端:
#!/usr/bin/python3 #-*- conding: utf-8- from socket import * ipaddr = '192.168.16.148' port = 8080 recv_size = 1024 udp_server = socket(AF_INET, SOCK_DGRAM) udp_server.bind((ipaddr, port)) data,addr = udp_server.recvfrom(recv_size) print(data) print(addr)
客户端:
#!/usr/bin/python3 #-*- conding: utf-8- from socket import * ipaddr = '192.168.16.148' port = 8080 udp_client = socket(AF_INET, SOCK_DGRAM) msg = input(">>: ") udp_client.sendto(msg.encode("utf-8"), (ipaddr, port))
10.使用TCP实现SSH功能
服务端:
#!/usr/bin/python3 #-*- conding: utf-8- import socket import subprocess ipaddr = '192.168.16.148' port = 8000 back_log = 5 receive_size = 1024 ssh_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssh_server.bind((ipaddr, port)) ssh_server.listen(back_log) while True: conn,addr = ssh_server.accept() while True: cmd = conn.recv(receive_size) if not cmd: break # 当客户端发送的命令为空的时候结束当前的循环 # print(cmd) # 使用subprocess模块运行客户端发送的命令 #这里subprocess可以执行b类型的字符,乱的要死,但是为了规范,还是建议转成str类型 res_cmd = subprocess.Popen(cmd, shell=True, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) err_cmd = res_cmd.stderr.read() # 当客户端发送的命令运行错误的时候 if err_cmd: conn.send("Command Error".encode("utf-8")) continue # 当客户端发送的命令运行正常的时候 else: out_cmd = res_cmd.stdout.read() conn.send(out_cmd) conn.close()
客户端:
#!/usr/bin/python3 #-*- conding: utf-8- import socket ipaddr = '192.168.16.148' port = 8000 recv_size = 1024 ssh_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssh_client.connect((ipaddr, port)) while True: cmd = input(">>>: ") if cmd == "exit" or cmd == "quit": break elif not cmd: continue # 当用户输入为空的时候进入下一个循环 ssh_client.send(cmd.encode('utf-8')) res_cmd = ssh_client.recv(recv_size) print(res_cmd.decode('utf-8')) ssh_client.close()
11.粘包
在介绍什么时粘包之前我们先来了解一下TCP和UDP:
- TCP:TCP是面向连接的 , 面向流的 , 提供高可靠性服务 . 收发两端都要一 一对应的socket。因此发送端为了更有效地将多个包发送到对端使用了一个优化算法(Nagle算法),该算法将多次发送的间隔小、数量小的数据包整合到一个大的数据块中进行封装。这时就需要提供一个合适的拆包机制才能合理分辨每一个数据包。
- UDP:UDP 是无连接的 , 面向消息的 , 不使用块的合并优化算法的服务,由于UDP支持的是一对多的模式,所以在缓冲区采用了链式结构来记录每一个到达的UDP包,在每一个UDP包中都有消息头(消息来源地址和端口信息),这样就很容易进行区分处理了。
- TCP和UDP的区别:TCP是基于数据流,于是在收发消息的时候不能为空,这就需要在客户端和服务端都添加相应的处理机制才能避免程序卡住或进入死循环;而UDP是基于数据报的,就算内容为空UDP也会自动加上消息头。
那什么是粘包?粘包从字面意思理解就是两个不同的数据包粘在了一起,就向如下的现象:
客户端
import socket ipaddr = '127.0.0.1' port = 8000 back_log = 5 recv_size = 1024 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((ipaddr, port)) server.listen(back_log) conn,addr = server.accept() data1 = conn.recv(recv_size) data2 = conn.recv(recv_size) print("第一次:%s" %data1) print("第二次:%s" %data2)
客户端
import socket ipaddr = '127.0.0.1' port = 8000 recv_size = 1024 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((ipaddr, port)) client.send("hello".encode('utf-8')) client.send("word".encode('utf-8'))
客户端收到的结果
第一次:b'helloword' 第二次:b''
这时第一种粘包情况:当发送端的数据小且间隔时间短时会造成两个包变为一个包
还要第二种粘包情况:当发生的数据过多时,接收端可能只接收到一部分内容,导致剩余的内容和下一个包的内容粘上;使用上面SSH的代码可以模拟该情况:
现象:
>>: ifconfig ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.16.148 netmask 255.255.255.0 broadcast 192.168.16.255 inet6 fe80::b61a:c99:edfb:528e prefixlen 64 scopeid 0x20<link> ether 00:0c:29:96:86:63 txqueuelen 1000 (Ethernet) RX packets 45906 bytes 11715999 (11.1 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 4821 bytes 2212944 (2.1 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> loop txqueuelen 1000 (Local Loopback) RX packets 18548 bytes 7402001 (7.0 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 18548 bytes 7402001 (7.0 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 virbr0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.12 >>: ls 2.255 ether 52:54:00:07:02:e6 txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 >>: ls socket_ssh_client.py socket_ssh_server.py tcp_server.py upd_server.py
其实导致粘包的主要原因时因为接收方不知道消息之间的界限,不知道一次性需要提前多少字节。
粘包的解决方法:
我们既然知道了造成粘包的原因是接收方不知道消息之间的界限,那我们就给消息包前加上一个包含消息大小的消息头:
服务端
#!/usr/bin/python3 #-*- conding: utf-8- # Filename: socket_ssh_server.py import socket import subprocess import struct ipaddr = '192.168.16.148' port = 8000 back_log = 5 receive_size = 1024 ssh_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssh_server.bind((ipaddr, port)) ssh_server.listen(back_log) while True: conn,addr = ssh_server.accept() while True: cmd = conn.recv(receive_size) if not cmd: break # print(cmd) res_cmd = subprocess.Popen(cmd, shell=True, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) err_cmd = res_cmd.stderr.read() if err_cmd: conn.send("Command Error".encode("utf-8")) continue else: out_cmd = res_cmd.stdout.read() # 发送一个4个字节的并包含信息长度的报头 conn.send(struct.pack('i', len(out_cmd))) conn.send(out_cmd) conn.close()
客户端
#!/usr/bin/python3 #-*- conding: utf-8- # Filename: socket_ssh_client.py import socket import subprocess import struct ipaddr = '192.168.16.148' port = 8000 receive_size = 1024 ssh_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssh_client.connect((ipaddr, port)) while True: cmd = input(">>: ") ssh_client.send(cmd.encode("utf-8")) # 接收信息长度 res = ssh_client.recv(4) # 解包信息长度并获取 length = struct.unpack('i', res)[0] data = ssh_client.recv(length) print(data.decode("utf-8"))
运行结果
>>: ls socket_ssh_client.py socket_ssh_server.py tcp_server.py upd_server.py >>: ifconfig ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.16.148 netmask 255.255.255.0 broadcast 192.168.16.255 inet6 fe80::b61a:c99:edfb:528e prefixlen 64 scopeid 0x20<link> ether 00:0c:29:96:86:63 txqueuelen 1000 (Ethernet) RX packets 50285 bytes 12012009 (11.4 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 5870 bytes 2513814 (2.3 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> loop txqueuelen 1000 (Local Loopback) RX packets 20051 bytes 7920159 (7.5 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 20051 bytes 7920159 (7.5 MiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 virbr0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 inet 192.168.122.1 netmask 255.255.255.0 broadcast 192.168.122.255 ether 52:54:00:07:02:e6 txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 >>: ls socket_ssh_client.py socket_ssh_server.py tcp_server.py upd_server.py
这里课堂上的方式也是一样,代码逻辑可能不太相同,但是意思差不多,这里还是贴出来,课堂上的思路是客户端多发送一个确认代码给服务端,如果服务端确认接收的代码和客户端协商的代码是同一个,那么服务端才发送结果给客户端,从而分开了2个包一起发送,解决了沾包的问题
服务端
#!/usr/bin/env python # -*- coding:utf-8 -*- ''' 改代码只能在linux系统下测试 ''' import socket,subprocess ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.bind(ip_port) sk.listen(5) while True: print ('server waiting...') conn,addr = sk.accept() while True: #接收来自客户端的命令并打印出来(不管是发送还是接收的都是bytes格式) client_data = conn.recv(1024) print ("client_cmd:",str(client_data,'utf8')) #如果客户端没有数据,直接跳出这次循环,一般是客户端断开后防止死循环 if not client_data:break; #将客户端的命令转成成为str用subprocess执行 cmd = str(client_data,'utf8').strip() cmd_call = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE) cmd_result = cmd_call.stdout.read() #如果客户端返回的结果==0,那么代表执行的就是如cd,或者命令执行出错了,那么就返回我们自定义的提示 if len(cmd_result) == 0: cmd_result = b'cmd executio has no output' #(不管是发送还是接收的都是bytes格式) ack_msg = bytes("CMD_RESULT_SIZE|%s" %len(cmd_result),'utf8') conn.send(ack_msg) #粘包的处理方式,多接受一次来次客户端的确认消息 client_ack = conn.recv(50) #如果客户阶段发来的字符串是CLIENT_READY_TO_RECV,那么才发送cmd结果过去 if str(client_ack,'utf8') == "CLIENT_READY_TO_RECV": conn.send(cmd_result) #import time #临时粘包的处理方式,不推荐 #time.sleep(1) conn.close()
客户端
#!/usr/bin/env python # -*- coding:utf-8 -*- import socket import time #!/usr/bin/env python # -*- coding:utf-8 -*- import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.connect(ip_port) while True: user_input = input("cmd>>:").strip() #如果用户输入为0,跳出这次循环,进入下一次循环,这个空格不会到服务端 if len(user_input) == 0:continue #如果用户输入q直接退出整个shell客户端 if user_input == 'q':break #把用户输入的命令发送给服务端(发送给服务端的格式只能是bytes) sk.send(bytes(user_input,'utf-8')) server_ack_msg = sk.recv(100) print ("server_response",str(server_ack_msg,'utf8')) cmd_res_msg = str(server_ack_msg,'utf8').split('|') if cmd_res_msg[0] == "CMD_RESULT_SIZE": cmd_res_size = int(cmd_res_msg[1]) #多发送一个确认代码给服务端,避免一条命令过长有沾包的问题 sk.send(b"CLIENT_READY_TO_RECV") #字符串的总变量,所有接收到的字符串都集中在这里 res = "" #客户端接收的长度 received_size = 0 #如果客户端接收的长度小于总长度,那么代表没有接收完成,那么就需要一直接收 while received_size < cmd_res_size: #每次接收500 data = sk.recv(500) #并且更改接收长度 received_size += len(data) #并且把每次接收的字符汇总到res里面 res += str(data,'utf8') else: #如果客户端接收的长度不小于总长度,那么代表接收完成,可以打印了 print (res) print ("-----------recv done-----------") sk.close()
12.使用socketserver模块实现socket并发
服务端
import socketserver import subprocess import struct class Mysocket(socketserver.BaseRequestHandler): def handle(self): # 获取连接 conn = self.request # 获取客户端地址 add = self.client_address print(conn, add) # 进入收发循环 while True: # 接收客户端发送的命令 cmd = conn.recv(1024) if not cmd: continue print(cmd) # 执行客户端发送的命令 res_cmd = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err_cmd = res_cmd.stderr.read() out_cmd = res_cmd.stdout.read() if err_cmd: conn.sendall(struct.pack("i", len("命令错误!"))) conn.sendall("命令错误!") else: conn.sendall(struct.pack("i", len(out_cmd))) conn.sendall(out_cmd) if __name__ == '__main__': ipaddr = '127.0.0.1' port = 8000 server = socketserver.ThreadingTCPServer((ipaddr, port), Mysocket) server.serve_forever()
客户端
import socket import struct ipaddr = '127.0.0.1' port = 8000 client1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client1.connect((ipaddr, port)) while True: cmd = input(">>: ") client1.sendall(cmd.encode('utf-8')) res = client1.recv(4) # print(res) length = struct.unpack('i', res)[0] data = client1.recv(length) print(data.decode(''))
转载:https://www.jianshu.com/p/a550673a4a25