python网络编程
知识内容:
1.socket语法及相关
2.黏包
3.struct模块
4.subprocess模块
5.socketserver模块
6.验证客户端连接的合法性
参考:
http://www.cnblogs.com/Eva-J/articles/8244551.html
http://www.cnblogs.com/alex3714/articles/5227251.html
关于网络基础概念:http://www.cnblogs.com/wyb666/p/9014857.html
一、socket语法及相关
1.基于TCP协议的socket
TCP是基于连接的,必须先启动服务端,然后再启动客户端去链接服务端;另外双方一定要有一方发消息一方收消息,不能同时收消息或同时发消息
TCP编程中用到的socket模块中的方法如下:
- socket([family, [, type, [proto]]]): 创建一个socket对象
- connect(address): 连接远程主机
- send(bytes[, flags]): 发送数据
- recv(bufsize[, flags]): 接受数据
- bind(address): 绑定地址
- listen(backlog): 开始监听,等待客户端连接
- accept(): 响应客户端的请求
- close(): 关闭连接或关闭服务器
server.py
1 # __author__ = "wyb" 2 # date: 2018/5/8 3 4 import socket # 导入socket模块 5 sk = socket.socket() 6 sk.bind(('127.0.0.1', 8888)) # bind(('ip', 'port')) 绑定ip和端口号 7 sk.listen() # 监听连接 8 9 conn, addr = sk.accept() # 接收客户端连接 10 11 res = conn.recv(1024) # 接收信息 12 print(res) 13 conn.send(b'hi!') # 发送信息,必须传一个bytes类型 14 res = conn.recv(1024) 15 print(res.decode('utf-8')) # 解码 16 conn.send(bytes("吃包子啊!".encode('utf-8'))) # 编码 17 18 conn.close() # 关闭连接 19 sk.close() # 关闭服务器 20 21 22 # 有收必有发,收发必相等
client.py
1 # __author__ = "wyb" 2 # date: 2018/5/8 3 import socket 4 5 sk = socket.socket() 6 7 sk.connect(('127.0.0.1', 8888)) # 连接服务器 8 sk.send(b'wyb') # 发送信息 9 res = sk.recv(1024) # 接受信息 10 print(res) 11 sk.send(bytes("中午吃什么".encode('utf-8'))) # 编码 12 res = sk.recv(1024) 13 print(res.decode("utf-8")) # 解码 14 15 sk.close() # 关闭客户端
2.基于UDP协议的socket
udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接,而是直接向接收方发送信息
UDP编程中用到socket模块中的如下方法:
(1)socket([family, [, type, [proto]]]): 创建一个socket对象,参数如下:
- family为socket.AF_INET表示IPV4,family为socket.INET6表示IPV6
- type为SOCK_STREAM表示TCP,type为SOCK_DGRAM表示UDP
- proto为协议号,通常为零,可以省略
(2)sendto(string, address):把string指定的内容发送给address指定的地址,其中address是一个包含接受方主机IP地址和应用进程端口号的元组,
格式为(IP地址, 端口号)
(3)recvfrom(bufsize[, flags]):接受数据,bufsize是可接受的最大数据,单位为kb
(4)bind(address): 绑定地址
(5)close(): 关闭服务器
server.py
1 # __author__ = "wyb" 2 # date: 2018/5/9 3 import socket 4 5 sk = socket.socket(type=socket.SOCK_DGRAM) # DGRAM datagram 6 sk.bind(('127.0.0.1', 8888)) # '127.0.0.1', 8888 -> server端地址 7 8 msg, addr = sk.recvfrom(1024) # 接受消息 9 print(msg.decode("utf-8")) 10 sk.sendto(b"bye", addr) # 发送消息 addr -> client端地址 11 12 sk.close() 13 # UDP的server不需要进行监听,也不需要建立连接,在启动服务之后只能被动的等 14 # 客户端发送消息过来,客户端发送消息的同时还会自带地址信息 15 # 消息回复的时候,不仅需要发送消息也要填上对方的地址
client.py
1 # __author__ = "wyb" 2 # date: 2018/5/9 3 import socket 4 5 sk = socket.socket(type=socket.SOCK_DGRAM) # DGRAM datagram 6 7 ip_port = ('127.0.0.1', 8888) # 发送的地址 '127.0.0.1', 8888 -> server端地址 8 9 sk.sendto(b"hello", ip_port) # 发送消息 10 ret, addr = sk.recvfrom(1024) # 接受消息 addr -> server端地址 11 print(ret.decode("utf-8")) 12 13 sk.close()
3.相关练习
(1)话痨对话
server.py
1 # __author__ = "wyb" 2 # date: 2018/5/8 3 import socket 4 5 sk = socket.socket() 6 7 sk.bind(('127.0.0.1', 8888)) 8 sk.listen() 9 10 conn, addr = sk.accept() 11 12 while True: 13 res = conn.recv(1024).decode('utf-8') # 接收消息(解码) 14 if res == "bye": # 退出 15 conn.send(b"bye") 16 break 17 print(res) # 输出消息 18 mes = input(">>>") # 输入消息 19 conn.send(mes.encode('utf-8')) # 发送消息(编码) 20 21 conn.close() 22 sk.close()
client.py
1 # __author__ = "wyb" 2 # date: 2018/5/8 3 import socket 4 5 sk = socket.socket() 6 7 sk.connect(('127.0.0.1', 8888)) 8 9 while True: 10 msg = input(">>>") # 输入消息 11 sk.send(msg.encode('utf-8')) # 传输消息(编码) 12 res = sk.recv(1024).decode("utf-8") # 接收消息(解码) 13 if res == 'bye': # 退出 14 sk.send(b'bye') 15 break 16 print(res) # 输出消息 17 18 sk.close()
(2)时间服务器
server.py
1 # __author__ = "wyb" 2 # date: 2018/5/8 3 import socket 4 from time import strftime 5 6 ip_port = ('127.0.0.1', 8888) # server端地址 7 8 udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 9 udp_server.bind(ip_port) 10 11 while True: 12 msg, addr = udp_server.recvfrom(1024) # 接受消息 13 print(msg.decode("utf-8")) 14 15 # 根据接受到的消息进行处理 16 if not msg: 17 time_fmt = '%Y-%m-%d %X' 18 else: 19 time_fmt = msg.decode('utf-8') 20 # 根据时间格式生成时间信息 21 back_msg = strftime(time_fmt) 22 23 # 发送消息 24 udp_server.sendto(back_msg.encode("utf-8"), addr) 25 26 27 udp_server.close()
client.py
1 # __author__ = "wyb" 2 # date: 2018/5/8 3 import socket 4 5 ip_port = ('127.0.0.1', 8888) 6 udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 7 8 while True: 9 msg = input('请输入时间格式(例%Y %m %d)>>: ').strip() # 输入信息 10 udp_client.sendto(msg.encode('utf-8'), ip_port) # 发送信息 11 12 data = udp_client.recv(1024) # 接受消息 13 print(data.decode("utf-8")) 14 15 16 udp_client.close()
(3)QQ聊天
server.py
1 import socket 2 3 ip_port = ('127.0.0.1', 8005) 4 sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 5 sk.bind(ip_port) 6 7 while True: 8 data, addr = sk.recvfrom(1024) # 接受消息 9 if data == b"bye": 10 break 11 print(data.decode("utf-8")) 12 info = input(">>>").encode("utf-8") 13 sk.sendto(info, addr) 14 15 sk.close()
client.py
1 import socket 2 3 ip_port = ('127.0.0.1', 8005) 4 sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 5 6 while True: 7 msg = input("老王: ") 8 msg = "\033[34m来自老王的消息: %s\033[0m" % msg 9 sk.sendto(msg.encode("utf-8"), ip_port) 10 data, addr = sk.recvfrom(1024) 11 if data == b"bye": 12 break 13 print(data.decode("utf-8")) 14 15 sk.close()
(4)远程执行命令
server.py
1 import socket 2 sk = socket.socket() 3 4 sk.bind(('127.0.0.1', 8888)) 5 sk.listen() 6 7 conn, addr = sk.accept() 8 9 while True: 10 cmd = input(">>>") 11 if cmd == 'q' or cmd == 'exit': 12 conn.send(b'q') 13 break 14 conn.send(cmd.encode("gbk")) 15 res = conn.recv(1024).decode("gbk") 16 print(res) 17 18 19 conn.close() 20 sk.close()
client.py
1 import socket 2 import subprocess 3 sk = socket.socket() 4 5 sk.connect(('127.0.0.1', 8888)) 6 7 while True: 8 cmd = sk.recv(1024).decode("gbk") 9 if cmd == "q" or cmd == "exit": 10 break 11 res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 12 sk.send(res.stdout.read()) 13 sk.send(res.stderr.read()) 14 15 16 sk.close()
二、黏包
1.什么是黏包
黏包问题是因为发送方把若干数据发送,接收方收到数据时候黏在一包,从接受缓冲区来看,后一包的数据黏在前一包的尾部,当连续send多个小数据包时就可能会发生黏包,其是TCP内部的优化算法造成的
注只有TCP才有黏包问题,UDP没有黏包问题:
1 UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。 2 不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 3 对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 4 不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
2.黏包成因
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据
补充说明:
1 用UDP协议发送时,用sendto函数最大能发送数据的长度为: 2 65535- IP头(20) – UDP头(8)=65507字节 3 用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误,将会丢弃这个包,不进行发送 4 5 用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制,只是暂不考虑缓冲区的大小,是指在用send函数时,数据长度参数不受限制 6 而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送
3.黏包具体情况
(1)发送方的缓存机制
连续send两个小包时,就会黏包,TCP会将这两个包合为一体,变成一个包 eg: 2+8 -> 10,其本质是发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,多个小数据会当做一个包发出去,产生粘包)
示例:
server.py
1 import socket 2 3 sk = socket.socket() 4 sk.bind(("127.0.0.1", 8888)) 5 sk.listen() 6 7 conn, addr = sk.accept() 8 res = conn.recv(200) 9 print("res:", res) 10 11 conn.close() 12 sk.close()
client.py
1 import socket 2 3 sk = socket.socket() 4 sk.connect(("127.0.0.1", 8888)) 5 sk.send(b"hello, world!") 6 sk.send(b"wyb666") 7 8 sk.close()
(2)接受方的缓存机制
连续两个recv,第一个recv特别小,假如说此时发送的是一个长数据,则会出问题(没接受完的数据缓存下来),其本质是接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
示例:
server.py
1 import socket 2 3 sk = socket.socket() 4 sk.bind(("127.0.0.1", 8888)) 5 sk.listen() 6 7 conn, addr = sk.accept() 8 res = conn.recv(2) 9 res2 = conn.recv(10) 10 print("res:", res) 11 print("res2:", res2) 12 13 conn.close() 14 sk.close()
client.py
1 import socket 2 3 sk = socket.socket() 4 sk.connect(("127.0.0.1", 8888)) 5 sk.send(b"hello, world!") 6 7 sk.close()
总结:
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
4.黏包的解决方案
(1)解决原理
问题的根源在于接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是在发送包之前发送数据的大小,再根据数据的大小来接受数据
根据上述方法解决黏包如下所示:
server.py
1 import socket 2 sk = socket.socket() 3 4 sk.bind(('127.0.0.1', 8888)) 5 sk.listen() 6 7 conn, addr = sk.accept() 8 9 while True: 10 cmd = input(">>>") 11 if cmd == 'q': 12 conn.send(b'q') 13 break 14 conn.send(cmd.encode("gbk")) 15 num = conn.recv(1024).decode("utf-8") # 接受数据的大小 16 conn.send(b"ok") # 响应ok 17 res = conn.recv(int(num)).decode("gbk") # 根据数据大小接受数据 18 print(res) 19 20 21 conn.close() 22 sk.close()
client.py
1 import socket 2 import subprocess 3 4 sk = socket.socket() 5 sk.connect(('127.0.0.1', 8888)) 6 7 while True: 8 cmd = sk.recv(1024).decode("gbk") 9 if cmd == "q": 10 break 11 res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 12 std_out = res.stdout.read() # 原理是队列 所以只能读一次 13 std_err = res.stderr.read() # 原理是队列 所以只能读一次 14 sk.send(str(len(std_out)+len(std_err)).encode("utf-8")) # 发送数据的大小 15 sk.recv(1024) # 接收ok 16 sk.send(std_out) 17 sk.send(std_err) 18 19 20 sk.close()
这样解决的好处与坏处:
- 好处:确定我到底要接受多大的数据
- 坏处:多了一次交互
注意:
最好在文件中配置一个配置选项,指定每一次recv的大小,也就是指定buffer,最大为4096
当我们要发送大的数据时,应该明确的告诉接收方要发送多大的数据,以便能准确的接受到所有数据
以上的方法多用在文件传输的过程中,在大文件的传输过程中发送方一边读一边传,接收方一边收一边写
(2)解决黏包问题升级版 - 借助struct模块
server.py
1 import socket 2 import struct 3 sk = socket.socket() 4 5 sk.bind(('127.0.0.1', 8888)) 6 sk.listen() 7 8 conn, addr = sk.accept() 9 10 while True: 11 cmd = input(">>>") 12 if cmd == 'q': 13 conn.send(b'q') 14 break 15 conn.send(cmd.encode("gbk")) 16 num = conn.recv(4) # 接受数据的大小 17 num = struct.unpack('i', num)[0] # 将4个字节转化成整数 18 res = conn.recv(int(num)).decode("gbk") # 根据数据大小接受数据 19 print(res) 20 21 22 conn.close() 23 sk.close()
client.py
1 import socket 2 import struct 3 import subprocess 4 5 sk = socket.socket() 6 sk.connect(('127.0.0.1', 8888)) 7 8 while True: 9 cmd = sk.recv(1024).decode("gbk") 10 if cmd == "q": 11 break 12 res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 13 std_out = res.stdout.read() # 原理是队列 所以只能读一次 14 std_err = res.stderr.read() # 原理是队列 所以只能读一次 15 len_num = len(std_out)+len(std_err) # 数据的大小 16 num_py = struct.pack('i', len_num) # 把数据大小压缩为4个 17 sk.send(num_py) 18 sk.send(std_out) 19 sk.send(std_err) 20 21 22 sk.close()
三、struct模块
1.简单使用
作用:struct模块可以把一个类型,如数字,转成固定长度的bytes
基本用法:
1 # __author__ = "wyb" 2 # date: 2018/5/16 3 # 什么是固定长度的bytes 4 # 为什么要转化成固定长度的bytes 5 6 import struct 7 8 res = struct.pack('i', 4096) # 'i'代表int,就是即将要把一个数字转化成固定长度的bytes类型 9 print(res) 10 11 num = struct.unpack('i', res) 12 print(num) # 输出元组 13 print(num[0]) # 输出数
2.使用struct解决黏包
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
发送时 | 接收时 |
先发送struct转换好的数据长度4字节 | 先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 | 再按照长度接收数据 |
具体实例见上面的解决黏包问题升级版
我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时 | 接收时 |
先发报头长度 |
先收报头长度,用struct取出来 |
再编码报头内容然后发送 | 根据取出的长度收取报头内容,然后解码,反序列化 |
最后发真实内容 | 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容 |
实例:
server.py
1 import socket 2 import subprocess 3 import struct 4 import json 5 6 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 7 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 8 phone.bind(('127.0.0.1',8080)) 9 phone.listen(5) #阻塞的最大数 10 11 while True: 12 coon,addr = phone.accept() 13 print(coon,addr) 14 while True: #通信循环 15 # 收发消息 16 cmd = coon.recv(1024) #接收的最大数 17 print('接收的是:%s'%cmd.decode('utf-8')) 18 #处理过程 19 res = subprocess.Popen(cmd.decode('utf-8'),shell = True, 20 stdout=subprocess.PIPE, #标准输出 21 stderr=subprocess.PIPE #标准错误 22 ) 23 stdout = res.stdout.read() 24 stderr = res.stderr.read() 25 # 制作报头 26 header_dic = { 27 'total_size': len(stdout)+len(stderr), # 总共的大小 28 } 29 header_json = json.dumps(header_dic) #字符串类型 30 header_bytes = header_json.encode('utf-8') #转成bytes类型(但是长度是可变的) 31 #先发报头的长度 32 coon.send(struct.pack('i',len(header_bytes))) #发送固定长度的报头 33 #再发报头 34 coon.send(header_bytes) 35 #最后发命令的结果 36 coon.send(stdout) 37 coon.send(stderr) 38 coon.close() 39 phone.close()
client.py
1 import socket 2 import struct 3 import json 4 5 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 6 phone.connect(('127.0.0.1', 8080)) # 连接服务器 7 buffer = 1024 8 9 while True: 10 cmd = input('输入命令>>').strip() 11 phone.send(cmd.encode('utf-8')) # 发送消息 12 header_len = struct.unpack('i', phone.recv(4))[0] # 收报头的长度 13 header_bytes = phone.recv(header_len) # 接收报头 14 header_json = header_bytes.decode('utf-8') # 拿到json格式的字典 15 header_dic = json.loads(header_json) # 反序列化拿到字典 16 total_size = header_dic['total_size'] # 拿到数据的总长度 17 # 最后收数据 18 recv_size = 0 19 total_data = b'' 20 while recv_size < total_size: # 循环的收 21 recv_data = phone.recv(buffer) # buffer只是一个最大的限制 22 recv_size += len(recv_data) 23 total_data += recv_data # 最终的结果 24 print('返回的消息:%s' % total_data.decode('gbk')) 25 26 phone.close()
四、subprocess模块
作用:提供与系统交互的方法,与标准库中的其它模块相比,提供了一个更高级的接口。用于替换如下模块:
os.system() , os.spawnv() , os和popen2模块中的popen()函数,以及 commands()
简单使用:
1 import subprocess 2 3 command = input("请输入命令: ").strip() 4 res = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 5 6 print(res) 7 print("stdout: ", res.stdout.read().decode("gbk")) 8 print("stderr: ", res.stderr.read().decode("gbk"))
五、socketserver模块
作用:socketserver用于简化网络客户端与服务器端的实现
server.py
1 # __author__ = "wyb" 2 # date: 2018/5/18 3 import socketserver 4 5 class MyServer(socketserver.BaseRequestHandler): 6 def handle(self): # self.request相当于一个conn 7 while True: 8 msg = self.request.recv(1024) 9 print(msg.decode("utf-8")) 10 msg = input(">>>") 11 self.request.send(msg.encode("utf-8")) 12 13 if __name__ == '__main__': 14 server = socketserver.ThreadingTCPServer(('127.0.0.1', 8888), MyServer) 15 # thread 线程 16 server.serve_forever() # 让server一直运行
client.py
1 import socket 2 3 sk = socket.socket() 4 sk.connect(('127.0.0.1', 8888)) 5 while True: 6 msg = input(">>>") 7 if msg == "q": 8 sk.send(b"q") 9 break 10 sk.send(("老王: "+msg).encode("utf-8")) 11 res = sk.recv(1024) 12 print(res.decode("utf-8")) 13 14 sk.close()
关于socketserver源码:http://www.cnblogs.com/Eva-J/p/5081851.html
六、验证客户端连接的合法性
想实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现
hmac使用:
1 # __author__ = "wyb" 2 # date: 2018/5/18 3 import hmac # 类似hashlib 4 5 h = hmac.new(b"key", b"hello") # secret_key 要加密的bytes 6 7 secret_h = h.digest() # 生成密文 8 print(secret_h) 9 10 print(hmac.compare_digest(secret_h, b"xxaf")) # 对比密文与另一个密文
server.py
1 # __author__ = "wyb" 2 # date: 2018/5/18 3 import os 4 import hmac 5 import socket 6 7 secret_key = b"wyb" # 加密的密钥 8 9 sk = socket.socket() 10 sk.bind(("127.0.0.1", 8001)) 11 sk.listen() 12 13 14 def check_conn(conn): # 检查连接是否合法 15 msg = os.urandom(32) 16 conn.send(msg) 17 h = hmac.new(secret_key, msg) # 加密消息 18 digest = h.digest() 19 client_digest = conn.recv(1024) # 接受对方发送回的加密消息 20 return hmac.compare_digest(digest, client_digest) # 对比加密后的消息 21 22 23 conn, addr = sk.accept() 24 res = check_conn(conn) 25 if res: 26 print("合法的客户端") 27 conn.close() 28 else: 29 print("不合法的客户端") 30 conn.close() 31 sk.close()
client.py
1 # __author__ = "wyb" 2 # date: 2018/5/18 3 import hmac 4 import socket 5 6 secret_key = b"wyb" # 加密的密钥 7 8 sk = socket.socket() 9 sk.connect(("127.0.0.1", 8001)) 10 11 msg = sk.recv(1024) 12 h = hmac.new(secret_key, msg) 13 digest = h.digest() 14 sk.send(digest) 15 16 sk.close()
注:如果客户端不知道正确的密钥或不知道正确的加密方法那么将无法通过验证