python网络编程

Posted woz333333

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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