网络编程|套接字(socket模块)
网络编程是计算机之间的数据交互。数据传输的大致过程是,计算机A与计算机B通信。计算机A从本机的应用层,传输层,网络层,数据链路层和物理链路层发出信息,通过网络,传输到计算机B,通过计算机B的物理链路层,数据链路层,网络层,传输层到达应用层的应用程序。这也说明,通信本质上应用程序之间的数据交互。
数据从应用层到物理链路层有很多协议,想要发出信息,就必须遵守这些协议的规范。套接字socket模块就帮助开发者解决这中问题。
套接字有两种其中,一种是文件类型的套接字:AF_UNIX,一种是网络类型的套接字:AF_INET。文件类型的套接字可以应用在Linux系统,Linux系统一切皆文件。使用python经行网络编程,使用更多的是AF_INET网络类型的套接字。
TCP通信
socket通信
通信代码如下图。整个通信先开启服务端,服务端运行到接受客服端链接时暂停,等待客户端发来的信号开始执行之后的程序。这时启动客户端,客户端发送信息,服务端接受到后从对象中取出数据,最大1024个字节,然后打印hello回复信息。关闭通信。客服端也一样,收到信息。进行打印,关闭通信。其中服务端有一个连接池的概念,这个链接池限制链接客户端的个数。
# 服务端 import socket # 调用socket模块 server = socket.socket() # 创建一个套接字对象 server.bind(('192.168.12.183', 8080)) # 服务端自己的IP和端口 IPv4即可,端口自己配置,在8000以上最好 server.listen(5) # 监听链接, 称便连接池 conn, addr = server.accept() # 接受客服端链接,也可视为使双向连接通道建立完成。返回的conn是对象客服端套接字对象,addr是地址 data = conn.recv(1024) # 数据接受 print(data) # 打印接收数据 conn.send(b'word!') # 回复信息 conn.close() # 断开通道 server.close() # 关闭通信 >>>b'hello' #客户端 import socket client = socket.socket() client.connect(('192.168.12.183', 8080)) # 链接服务端,服务端IP与端口 client.send(b'hello') # 发送数据 data = client.recv(1024) # 数据接受 print(data) # 打应数据 client.close() # 关闭通道 >>>b'word!'
在通信的时候一般都是可以经行循环通信。在上面的基础上可以改进此通信。使服务端可以随时经行通信。
# 服务端 import socket server = socket.socket() server.bind(('192.168.12.183', 8080)) # 服务端自己的IP和端口 server.listen(5) conn, addr = server.accept() # 建立通道 在此基础上可以经行数据传输 while True: data = conn.recv(1024) print(data) conn.send(b'word!') # 客户端 import socket client = socket.socket() client.connect(('192.168.12.183', 8080)) while True: msg = input('>>>').strip() client.send(msg.encode('utf-8')) data = client.recv(1024) print(data)
连接循环。对输入和接受的数据进行逻辑处理是程序更加健壮。
# 服务端 import socket server = socket.socket() server.bind(('192.168.12.183', 8080)) # 服务端自己的IP和端口 server.listen(5) while True: conn, addr = server.accept() # 建立通道 在此基础上可以经行数据传输 while True: try: data = conn.recv(1024) print(data) if len(data) == 0: break # 客服端异常退出后会循环打印b'',这个时候需要做个判断 conn.send(b'word!') except ConnectionResetError: break conn.close() # 客户端 import socket client = socket.socket() client.connect(('192.168.12.183', 8080)) while True: try: msg = input('>>>').strip() if not msg: continue client.send(msg.encode('utf-8')) data = client.recv(1024) print(data) except ConnectionResetError as e: print(e) break
在使用TCP通信时,但多个数据包连续(时间间隔比较短时会出现黏包)发送时,接受方会将多个包同时接受以先后顺序,队列的方式放在一起。接收方会无法将多个文件经行分离。这是一种黏包现象。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 服务端 import socket server = socket.socket() server.bind(('127.0.0.1', 8081)) server.listen(5) while True: conn, addr = server.accept() data = conn.recv(1024) print(data.decode('utf-8')) conn.send(b'hello') conn.send(b'hello') conn.send(b'hello') conn.send(b'hello') conn.send(b'hello') >>>aaa # 客户端 import socket client = socket.socket() client.connect(('127.0.0.1', 8081)) while True: msg = input('>>>').encode('utf-8') client.send(msg) data = client.recv(1024) print(data) data = client.recv(1024) print(data) data = client.recv(1024) print(data) data = client.recv(1024) print(data) data = client.recv(1024) print(data) b'hello' b'hellohellohello' b'hello'
解决黏包现象可以用报头的方式解决这种问题。利用报头固定大小,携带数据大小的属性。告诉接收者发送到数据有多大,按照大小接受。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 客户端 import socket import struct import json client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: try: com = input('>>>').strip() if len(com) == 0: continue client.send(com.encode('utf-8')) header = client.recv(4) # 先收报头 dict_l = struct.unpack('i',header)[0] # 解压报头获取携带字典大小数据 j_dict = client.recv(dict_l) # 按字典大小接收字典 dict_m = json.loads(j_dict.decode('utf-8')) # 还原字典 a = 0 msg = b'' if a < dict_m['file_msg']: # dict_m['file_msg'] 获取字典中携带要发送数据包的大小 data = client.recv(1024) msg += data a += len(data) print(msg) except BaseException: break # 服务端 import socket import json import struct import subprocess server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn, addr = server.accept() while True: try : com = conn.recv(1024) if len(com) == 0: break print(com) obj = subprocess.Popen(com.decode('utf-8'),shell =True, stdout = subprocess.PIPE, stderr= subprocess.PIPE) res = obj.stdout.read() + obj.stderr.read() # 要发送信息 dict_s = {'name':'命令信息', 'file_msg': len(res)} # 将要发送信息放入字典中 json_d = json.dumps(dict_s) # 将字典序列化 header = struct.pack('i',json_d) # 将序列化的字典大小信息放入报头 conn.send(header) # 先发报头 conn.send(json_d.encode('utf-8')) # 在发序列化后的字典 conn.send(res) # 最后发要发送到信息 except BaseException: break conn.close()
UDP通信
UDP通信,自带报头,不存在粘包现象。与TCP协议下通信相比,UDP协议下通信不需要建立双向通道,直接向对方地址发送。支持一对一,和一对多,可进行并发通信。
UDP通信的有四个特点。客户端允许发送,不会粘包,支持并发,客户端不存在也不会出错。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 客户端 import socket server = socket.socket(type=socket.SOCK_DGRAM) client_addr = ('127.0.0.1', 8080) # 服务端地址端口 while True: msg = input('>>>') server.sendto(msg.encode('utf-8'), client_addr) # 发数据包 data, addr = server.recvfrom(1024) # 收数据包 print(data) # 服务端 import socket server = socket.socket(type=socket.SOCK_DGRAM) server.bind(('127.0.0.1', 8080)) # 服务端自己的地址和端口 while True: data, addr = server.recvfrom(1024) # 收数据包,返回数据包和客户端地址 print(data) msg = 'hello' server.sendto(msg.encode('utf-8'), addr) # 发数据,按返回的客户端地址发送
socketserver模块。使TCP协议下通信和UDP协议下通信一样灵活。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 客户端 import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: client.send(b'hello') data = client.recv(1024) print(data.decode('utf-8')) # 服务端 import socketserver class MyServer(socketserver.BaseRequestHandler): # 设计一个类,继承socketserver模块下BaseRequestHandler这个类 def handle(self): while True: try: data = self.request.recv(1024) # 接收数据 print(data.decode('utf-8')) self.request.send(b'ok') # 发送数据 except ConnectionResetError as e: print(e) break if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.01', 8080), MyServer) # 创建对象 server.serve_forever() # 启动对象