【网络编程】 �

原文: http://blog.gqylpy.com/gqy/227

目录

1.socket层

2.理解socket

3.套接字的发展史

4.网络基础

5.socket基本操作

6.socket进阶

7.黏包

8.解决黏包

8.socket的更多方法


1.socket层

![在这里插入图片描述](http://blog.gqylpy.com/media/ai/2019-03/5ccf086a-baa7-4268-a9f9-a50fb74136b7.png)

2.理解socket

socket是应用层与TCP/IP协议族的中间软件抽象层,它是一组接口。在设计模式中,socket其实就是一个面膜,它把复杂的TCP/IP协议族隐藏在socket接口后面,对于用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。



3.套接字的发展史

套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的Unix,即人们所说的BSD Unix。因此,有时人们也把套接字称为“伯克利套接字”或“BSD套接字”。一开始,套接字被设计用在同一台主机上多个应用程序之间的通讯,这也被称为进程间通讯或IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。

  • 基于文件类型的套接字家族:AF_UNIX

unix下一切皆文件,基于文件的套接字调用的就是底层的文件系统来获取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

  • 基于网络类型的套接字家族:AF_INET

还有AF_INET6被用于ipv6,以及一些其他的地址家族,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现。所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,由于我只关心网络编程,所以大部分时候我只使用AF_INET



4.网络基础

  • TCP(Transmission Control Protocol)协议

可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、无边界的字节流。使用TCP的应用:Web浏览器、电子邮件、文件传输程序。

tcp本质上在同一时间只允许一个客户端连接,当应用程序希望通过TCP与另一个应用程序通信时,他会发送一个通信请求,此请求必须被送到一个确切的地址,在双方“握手”之后,TCP将在两个应用程序之间建立一个全双工(full-duplex)的通信,全双工的通信将占用两台计算机之间的通信线路,直到他被一方或双方关闭为止。

TCP是英特网中的传输层协议,使用三次握手建立连接,当主动方发出SYN连接请求后,等待对方回答SYN+ACK[1],最终对方的SYN执行ACK确认,这种建立建立连接的方法可以防止错误的连接。

TCP三次握手与四次挥手基本流程:

![在这里插入图片描述](http://blog.gqylpy.com/media/ai/2019-03/98891c12-bc48-48a9-ac2e-c8e20520974a.png)

三次握手:
一次:客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态.
二次:服务器端收到SYN报文,并回应一个SYN(SEQ=y).ACK=x+1)报文,进入SYN_RECV状态.
三次:客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+x)报文,进入Established状态.

说白话吧,tcp三次握手,第一次一定是client先发起请求的.
1.客户端先向服务端发送一条连接请求,用于确认服务端是否可连接.
2.服务端收到请求后,开始做相应的工作并返回确认信息.
3.客户端收到确认信息后,向服务端发送连接信息并建立连接.

四次挥手:
1.客户端向服务端发送端开连接的请求.
2.服务端收到请求后,开始做断开连接的工作,同时返回确认.
3.服务端已做完断开连接的工作,再次向客户端发送确认信息.
4.客户端收到确认信息后,向服务端发送断开信息以断开连接.

补充:建立和断开连接都是交换3次报文(SYN, ACK, FIN)


  • UDP(User Datagram Protocol)协议

不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS)、视频流、IP语音(VoIP)。udp允许一个服务器和多个客户端同时通信。

我知道说这些你们也不懂,直接上图:

![在这里插入图片描述](http://blog.gqylpy.com/media/ai/2019-03/314a9385-b40c-4fc8-9a1b-9f2ec7d477c2.png)

 


  • ARP(Address Resolution Protocol)协议

因为所有定义的协议都至少是在网络层以上的,所以在TCP/IP模型中,ARP协议属于IP层;又因为ARP协议是工作在数据链路层的,所以在OSI模型中,ARP协议属于链路层。

地址解析协议,查询IP地址与MAC地址的对应关系,根据IP地址来获取物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回的消息,以此确认目标的物理地址,收到返回消息后,将该IP地址和物理地址存入本机的ARP缓存表中,默认保留时间为5分钟,下次请求时将直接查询ARP缓存表,以节约资源。

地址解析协议时建立在网络中各个主机互相信任的基础上的,网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文后不会检测该报文的真实性,而是直接存储到ARP缓存表中。此后该主机发送的信息将无法到到预习的主机或网络收到限制或传输的信息被泄漏等

  • 交换机与路由器的区别

交换机的主要功能是组织局域网,经过交换机解析信息之后,将信息以点对点,点对多点的形式发送给固定端

路由器的主要功能是进行跨网段的数据传输,路由选择最佳路径

顺便说一下OSI五层模型:物理层-数据链路层-网络层-传输层-应用层


5.socket基本操作

  • 基于TCP协议的socket

tcp是面向连接的,可靠的,面向字节流形式的
因为tcp是基于连接的,所以必须先启动服务端,然后再启动客户端去链接服务端

  1. # server
  2. import socket
  3. sk = socket.socket() # 实例化一个socket的对象sk
  4. sk.bind(('127.0.0.1', 4096)) # 把要监听的ip和port绑定到对象sk
  5. sk.listen() # 监听链接
  6. conn, addr = sk.accept() # 阻塞程序,等待客户端链接
  7. ret = conn.recv(4096) # 接收数据,接收最多4096个字节
  8. print(ret.decode('utf-8')) # 打印客户端发来的数据,别忘了转码
  9. conn.send('嘿嘿嘿'.encode('utf-8')) # 向客户端发送信息
  10. conn.close() # 关闭客户端套接字
  11. sk.close() # 关闭服务器套接字(可选)
  1. # client
  2. import socket
  3. sk = socket.socket() # 实例化一个socket的对象sk
  4. sk.connect(('127.0.0.1', 4096)) # 把要连接的ip和port绑定到对象sk
  5. sk.send('嗨!大家好!'.encode('utf-8')) # 向服务端发送信息
  6. ret = sk.recv(9) # 接收服务端信息,接收最多9个字节
  7. print(ret.decode('utf-8'))
  8. sk.close() # 关闭客户端套接字

启动服务端遇到OSError报错的解决方法:

  1. # server
  2. import socket
  3. from socket import SOL_SOCKET, SO_REUSEADDR # ⚠️
  4. sk = socket.socket() # 实例化一个socket的对象sk
  5. sk.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # ⚠️
  6. sk.bind(('127.0.0.1', 4096)) # 把要监听的ip和port绑定到对象sk
  7. sk.listen() # 监听链接
  8. conn, addr = sk.accept() # 阻塞程序,等待客户端链接
  9. ret = conn.recv(4096) # 接收数据,接收最多4096个字节
  10. print(ret.decode('utf-8')) # 打印客户端发来的数据,别忘了转码
  11. conn.send('嘿嘿嘿'.encode('utf-8')) # 向客户端发送信息
  12. conn.close() # 关闭客户端套接字
  13. sk.close() # 关闭服务器套接字(可选)

  • 基于UDP协议的socket

udp是无连接的,不可靠的,面向字节流形式的
因为udp是无连接的,启动服务之后可以直接接受信息,不需要提前建立链接

  1. # server
  2. import socket
  3. sk = socket.socket(type=socket.SOCK_DGRAM) # 创建基于udp协议的服务器套接字
  4. sk.bind(('127.0.0.1', 4096)) # 将ip和port绑定到套接字sk
  5. msg, addr = sk.recvfrom(1024) # 接收客户端数据,接收最多1024个字节
  6. print(msg.decode('utf-8'))
  7. sk.sendto('嘿嘿嘿'.encode('utf-8'), addr) # 向客户端发送信息
  8. sk.close()
  1. # client
  2. import socket
  3. s = '嗨!大家好!'.encode('utf-8')
  4. ip_port = ('127.0.0.1', 4096)
  5. sk = socket.socket(type=socket.SOCK_DGRAM) # 创建基于udp的套接字
  6. sk.sendto(s, ip_port) # 向服务端发送信息
  7. msg, addr = sk.recvfrom(1024) # 接收服务端信息,接收最多1024个字节
  8. print(msg.decode('utf-8'), addr)
  9. sk.close()

  • socket参数详解:def __init__(self, family=-1, type=-1, proto=-1, fileno=None):

family:地址系列应为AF_INFT(默认值),AF_INET6, AF_UNIX, AF_CAN或AF_RDS.
               AF_UNIX域实际上是使用本地的socket文件来通讯.

type:套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM, SOCK_RAW或其他SOCK_常量之一.
            SOCK_STREAM是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送.
            SOCK_DGRAM是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播消息.

proto:协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下协议应为CAN_RAW或CAN_BCM之一.

fileno:如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回.
              与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的.
              这可能有助于使用socket.close()关闭一个独立的插座.



6.socket进阶

  • 文本文件传输

  1. # 发送端
  2. def sender(file_path=None, host='127.0.0.1', port=1024):
  3. """
  4. 必须传入第一个参数
  5. :param author: zyk
  6. :param file_path: 要传送的文件绝对路径
  7. :param host: 接收端的IP
  8. :param port: 接收端的端口
  9. """
  10. from os import path
  11. from hashlib import md5
  12. from time import sleep
  13. import socket
  14. file_name = path.basename(file_path) # 获取要传送的文件名称
  15. file_size = path.getsize(file_path) # 获取要传送的文件大小
  16. name_size = file_name + '/' + str(file_size)
  17. print("你要传送的文件名为:%s,大小为:%s字节" % (file_name, file_size))
  18. md5 = md5()
  19. sk = socket.socket()
  20. print("\n正在尝试连接接收端", end='')
  21. sleep(0.5);
  22. print('.', end='')
  23. sleep(0.3);
  24. print('.', end='')
  25. sleep(0.2);
  26. print('.', end='\n\n')
  27. sk.connect_ex((host, port)) # 尝试连接接收端
  28. sk.send(name_size.encode('utf-8')) # 传送文件信息
  29. print("连接成功,已发送文件信息\n等待接收端返回确认信息....")
  30. if sk.recv(1500).decode('utf-8') != 'y': # 判断接收端是否要接收文件
  31. print("接收端拒绝了接收");
  32. return
  33. with open(file_path, 'rb')as f:
  34. print("传送中", end='')
  35. while file_size > 0:
  36. read = f.read(1500) # 每次传送1500个字节
  37. md5.update(read) # 生成校验值
  38. sk.send(read)
  39. file_size -= 1500
  40. sleep(0.3) # 避免黏包,传一次停一会
  41. print('.', end='')
  42. print("\n校验中", end='')
  43. sleep(0.5);
  44. print('.', end='')
  45. sleep(0.3);
  46. print('.', end='')
  47. sleep(0.2);
  48. print('.', end='\n\n')
  49. verify = sk.recv(1500) # 接收端的校验值
  50. if verify.decode('utf-8') == md5.hexdigest(): # 校验文件
  51. sk.send(verify)
  52. print("文件传送成功!")
  53. else:
  54. sk.send("大家好,我是失败".encode('utf-8'))
  55. print("校验失败!")
  56. sk.close()
  1. # 接收端
  2. def receiving_end(file_path=None, ip='127.0.0.1', port=1024, encoding='GBK'):
  3. """
  4. 接收的文件默认存放到当前目录
  5. 中途退出程序后,可能会导致再次运行失败,此时可尝试更换监听端口
  6. :param author: zyk
  7. :param file_path: 接收的文件存放路径
  8. :param host: 监听的IP
  9. :param port: 监听的端口
  10. :encoding: 接收的文件的编码格式,windows一般不会报错,否则'utf-8'
  11. """
  12. from os import path
  13. from hashlib import md5
  14. from time import sleep
  15. import socket
  16. if not file_path:
  17. file_path = path.abspath('.') # 获取当前目录
  18. md5 = md5()
  19. sk = socket.socket()
  20. sk.bind((ip, port)) # 确定要监听的ip和端口
  21. sk.listen() # 开始监听
  22. print("等待发送端连接....")
  23. conn, addr = sk.accept() # 阻塞程序,等待客服端连接
  24. print("发送端已连接,发送端IP为:%s\n"
  25. "正在获取文件信息" % (addr[0]), end='')
  26. sleep(0.5);
  27. print('.', end='')
  28. sleep(0.3);
  29. print('.', end='')
  30. sleep(0.2);
  31. print('.', end='\n')
  32. name_size = conn.recv(1500).decode('utf-8') # 接收文件信息
  33. file_name, file_size = name_size.split('/')
  34. file_size = int(file_size)
  35. oper = input("文件名:%s\t文件大小:%s字节\n" # 判断用户操作
  36. "确认接收请输入'y', 否则取消接收该文件:"
  37. % (file_name, file_size))
  38. conn.send(oper.encode('utf-8')) # 返回操作
  39. if oper != 'y': return
  40. with open('zyk.' + file_name, 'w')as f: # 注意避免文件重名
  41. print("接收中", end='')
  42. while file_size > 0:
  43. ret = conn.recv(1500)
  44. md5.update(ret) # 生成校验值
  45. f.write(ret.decode('utf-8'))
  46. file_size -= 1500
  47. print('.', end='')
  48. print("\n校验中", end='')
  49. sleep(0.5);
  50. print('.', end='')
  51. sleep(0.3);
  52. print('.', end='')
  53. sleep(0.2);
  54. print('.', end='\n\n')
  55. conn.send(md5.hexdigest().encode('utf-8')) # 返回校验值
  56. if conn.recv(1500).decode('utf-8') == md5.hexdigest():
  57. print("文件接收成功!")
  58. else:
  59. print("校验失败!")
  60. conn.close()
  61. sk.close()
  • 验证客户端合法性

  1. # Server
  2. from socket import *
  3. import hmac, os
  4. length_bytes, encod, ip_port = 32, 'utf-8', ('127.0.0.1', 1024)
  5. salt = b"hi, I'm salt."
  6. random_bytes = os.urandom(length_bytes) # 生成随机bytes类型的数据
  7. server_results = hmac.new(salt, random_bytes).digest() # 服务端结果
  8. sk = socket(AF_INET, SOCK_STREAM)
  9. sk.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
  10. sk.bind(ip_port)
  11. sk.listen()
  12. conn, addr = sk.accept()
  13. conn.sendall(random_bytes) # 下发随机bytes
  14. client_results = conn.recv(16) # 接收结果
  15. if client_results != server_results: # 判断合法性
  16. print("连接不合法")
  17. else:
  18. print("合法连接")
  1. # Client
  2. from socket import *
  3. import hmac, os
  4. length_bytes, encod, ip_port = 32, 'utf-8', ('127.0.0.1', 1024)
  5. salt = b"hi, I'm salt."
  6. sk = socket()
  7. sk.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
  8. sk.connect_ex(ip_port)
  9. randoim_bytes = sk.recv(length_bytes) # 接收随机bytes
  10. client_results = hmac.new(salt, randoim_bytes).digest() # 计算结果
  11. sk.sendall(client_results) # 上传结果
  1. # 假的客户端
  2. from socket import *
  3. import hmac, os
  4. length_bytes, encod, ip_port = 32, 'utf-8', ('127.0.0.1', 1024)
  5. pseudo_salt = "大家好,我是假盐".encode('utf-8')
  6. sk = socket()
  7. sk.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
  8. sk.connect_ex(ip_port)
  9. random_bytes = sk.recv(32)
  10. client_results = hmac.new(pseudo_salt, random_bytes).digest()
  11. sk.sendall(client_results)

 

  • 聊天

  1. # server
  2. import socket
  3. ip_port = ('127.0.0.1', 4096)
  4. udp_sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  5. udp_sk.bind(ip_port)
  6. try:
  7. while 1:
  8. msg, addr = udp_sk.recvfrom(4096)
  9. print("来自[%s:%s]的一条消息:\033[1;36m%s\033[0m"
  10. %(addr[0], addr[1], msg.decode('utf-8')))
  11. bk_msg = input("回复消息:").strip()
  12. udp_sk.sendto(bk_msg.encode('utf-8'), addr)
  13. except Exception as e:
  14. print(e)
  15. finally:
  16. udp_sk.close()
  1. # client
  2. import socket
  3. BUFSIZE = 4096
  4. name_di = [('赵丽颖', ('127.0.0.1', 4096)),
  5. ('杨幂', ('127.0.0.1', 4096)),
  6. ('白百何', ('127.0.0.1', 4096)),
  7. ('迪丽热巴', ('127.0.0.1', 4096)),
  8. ]
  9. udp_sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  10. try:
  11. while 1:
  12. print("选择身份:")
  13. for s,i in enumerate(name_di, 1):
  14. print(s, i[0])
  15. try:
  16. sum = int(input("\n序号:").strip())
  17. if sum < 1:
  18. print("输入有误!\n")
  19. continue
  20. ip_port = name_di[sum-1][1]
  21. except Exception:
  22. print("输入有误!\n")
  23. continue
  24. msg = input("输入消息:").strip().encode('utf-8')
  25. udp_sk.sendto(msg, ip_port)
  26. bk_msg, addr = udp_sk.recvfrom(BUFSIZE)
  27. print("来自[%s:%s]的消息:\033[1;36m%s\033[0m"
  28. %(addr[0], addr[1], bk_msg.decode('utf-8')))
  29. except Exception as e:
  30. print(e)
  31. finally:
  32. udp_sk.close()


7.黏包

  • 黏包成因

1.在发送端发送的数据,接收端不知道该如何去接收,导致成数据混乱的情况.
2.在tcp协议中,有一个合包机制(Nagle算法),它会将多次发送的间隔较小且数据较小的数据进行打包,然后一次性发送.
3.还有一个拆包机制,在发送端,因为受到网卡的MTU限制,会导致超过MTU最大值限制的数据包被拆分成多个小的数据包进行传输,当传输到目标主机的操作系统层时,会重新将多个小的数据合并成原本的数据.


  • 基于TCP实现的黏包现象
  1. # Server
  2. from socket import *
  3. import subprocess
  4. ip_port = ('127.0.0.1', 1024)
  5. BUFSIZE = 1472
  6. sk = socket(AF_INET, SOCK_STREAM)
  7. sk.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
  8. sk.bind(ip_port)
  9. sk.listen(5)
  10. while True:
  11. conn, addr = sk.accept()
  12. print("客户端:", addr)
  13. while True:
  14. cmd = conn.recv(BUFSIZE)
  15. res = subprocess.Popen(cmd.decode('utf-8'),
  16. shell=True,
  17. stdout=subprocess.PIPE,
  18. stderr=subprocess.PIPE,
  19. stdin=subprocess.PIPE)
  20. stdout = res.stdout.read()
  21. stderr = res.stderr.read()
  22. conn.send(stdout) if stdout else conn.send(stderr)
  1. # Client
  2. from socket import *
  3. ip_port = ('127.0.0.1', 1024)
  4. BUFSIZE = 1472
  5. sk = socket(AF_INET, SOCK_STREAM)
  6. ret = sk.connect_ex(ip_port)
  7. while True:
  8. sk.send('pwd'.encode('utf-8'))
  9. act_res = sk.recv(BUFSIZE)
  10. print(act_res.decode('utf-8'), end='')

 


  • TCP协议的拆包机制

1.当发送端缓冲区的长度大于网卡的MTU时,tcp会将此次发送的数据拆分成几个小的数据包进行发送。

2.MTU(Maxinum Transmission Unit):意思是网路上传送的最大数据包,MTU的单位是字节,大部分网络设备的MTU都是1500,如果本机的MTU比网关的MTU大,那么大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包略,降低网络速度。


  • 面向流的通信特点和Nagle算法

1.TCP(Transport Control Protocol):传输控制协议,面向连接的,面向流的,提高可靠性服务。

2.收发两端都要有一一成对的socket,Nagle算法可以使发送端的数据包更有效的发送到接收端,它会将连续发送间隔(一般在200毫秒)较小且数据较小的数据合并成一个大的数据块,然后进行封包,一次发出去,这就导致接收端无法分辨数据包了,必须提供科学的拆包机制。

3.面向流的通信都是无消息保护边界的,对于空消息:tcp是基于数据流的,所以收发的消息不能为空,这就需要在首发两端做空消息的处理机制,防止程序卡住;udp是基于数据报的,因为udp协议会封装上消息头部的信息,所以即使时空字符也一样可以发送。

4.tcp协议的数据不会丢,没有收完的数据,下次接收,会继续上次的地方接收,接收方总是在收到ack时才会清除缓冲区的内容,所以数据是可靠的,但是会黏包。


  • 基于TCP协议特点的黏包现象成因
![在这里插入图片描述](http://blog.gqylpy.com/media/ai/2019-03/bdc4dd79-ac3a-4d17-8703-8da5f74edc7b.png)

1.发送端可以是1k1k的发送数据,而接收端的应用程序却可以是两k两k的提数据,当然也有可能是3k、6k或者一次只提1个字节的数据,也就是说,应用程序所看到的数据其实是一个整体,或者说是一个流(stream),一条消息有多少字节对应用程序来说是不可见的,因此TCP协议是面向流的协议,这也是容易出现黏包问题的原因

2.UDP协议是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP协议很不同的。

3.基于TCP的套接字的客户端往服务端上传文件时,上传的文件内容是一段一段的字节流发送的,在接收方看来,根本就不知道该字节流时从何处开始何处结束的。

4.发送方引起的黏包时由TCP协议本身造成的,TCP协议为了提高传输效率,往往会在收集足够多的数据之后才会发送一个TCP段(连续几次send的数据都很少,TCP会根据优化算法把这些数据合并成一个TCP段后,一次发出去),这就导致了接收方收到了黏包数据。


  • UDP协议不会有黏包现象

1.UDP(User Datagram Protocol):用户数据报协议,是无连接的,面向消息的,提供高效率的服务。

2.不会使用合并优化算法(Nagle),UDP支持一对多的模式,接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,分为包头和数据两部分。这样,对于接收端来说,很容易进行区分处理,即面向消息的通信是有消息保护边界的。

3.不可靠不黏包的UDP协议:UDP的recvfrom时阻塞的,一个recvfrom(x)必须对应唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y<x数据就会丢失后年的部分,所以UDP根本不会黏包,但是会有丢数据的情况,不可靠。

4.用UDP协议发送数据时,sendto函数最大能发送的数据长度为:65535 - IP头(20) - UDP头 = 65507字节,如果指定的数据长度大于该值,则函数会返回错误并丢弃这个包,不会发送。再联系到数据链路层,因为网卡的MTU一般被限制在了1500字节,所以对于数据链路层来说,一次收发的数据大小被限制在1500 -IP包头(20) - UDP包头(8) = 1472字节,如果数据的长度超过这个字节,就会被拆分,导致丢包率增加,所以比较理想的大小应该在1472字节一下。

5.用TCP协议发送消息时,send函数不会限制数据的长度,因为TCP协议是基于数据流的,不存在包大小的限制(暂不考虑缓冲区的大小),而实际上,所指定的这段数据并不一定会一次性发送出去,数据比较长时会被分段发出去,如果比较短,可能会等待和下一次send的数据一起发送。


  • 导致黏包的两种情况

情况一:发送方的缓存机制
发送端多次连续发送间隔较小,且数据较小的时候,可能会被Nagle算法合并成一个数据报发出去.

  1. # Server
  2. from socket import *
  3. ip_port = ('127.0.0.1', 1025)
  4. BUFSIZE = 22
  5. sk = socket(AF_INET,SOCK_STREAM)
  6. sk.bind(ip_port)
  7. sk.listen(5)
  8. conn, addr = sk.accept()
  9. data1 = conn.recv(BUFSIZE)
  10. data2 = conn.recv(BUFSIZE)
  11. print('-->', data1.decode('utf-8'))
  12. print('-->', data2.decode('utf-8'))
  13. conn.close()
  14. sk.close()
  1. # Client
  2. from socket import *
  3. ip_port = ('127.0.0.1', 1025)
  4. BUFSIZE = 1472
  5. sk = socket(AF_INET,SOCK_STREAM)
  6. res = sk.connect_ex(ip_port)
  7. sk.send('hi! '.encode('utf-8'))
  8. sk.send('Everybody is good!'.encode('utf-8'))
  9. sk.close()

情况二:接收方的缓存机制
接收方不及时接收缓存区的包,造成多个包存在缓存区中(客户端发送了一段数据,服务端只接收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,导致产程黏包)

  1. # Server
  2. from socket import *
  3. ip_port = ('127.0.0.1', 1025)
  4. BUFSIZE = 22
  5. sk = socket(AF_INET, SOCK_STREAM)
  6. sk.bind(ip_port)
  7. sk.listen(5)
  8. conn, addr = sk.accept()
  9. data1 = conn.recv(2) # 一次没有接收完整
  10. data2 = conn.recv(10) # 二次接收的时候,接着一次的数据接收
  11. print('-->', data1.decode('utf-8'))
  12. print('-->', data2.decode('utf-8'))
  13. conn.close()
  14. sk.close()
  1. # Client
  2. from socket import *
  3. ip_port = ('127.0.0.1', 1025)
  4. BUFSIZE = 1472
  5. sk = socket(AF_INET,SOCK_STREAM)
  6. res = sk.connect_ex(ip_port)
  7. sk.send('hei hei hei'.encode('utf-8'))

  • 小结

黏包现象只会发生在TCP协议中​​​​​​,从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、TCP协议面向字节流通信的特点,实际上还是因为接收方不知道消息的边界,不知道一次要提取多少字节的数据所造成的。



8.解决黏包

  • 基本思路

让发送端在发送数据之前,把自己要发送的字节流总大小让接收端知晓,然后接收端开始一个循环,来接收所有数据

![在这里插入图片描述](http://blog.gqylpy.com/media/ai/2019-03/bddf0396-479b-48fb-b41a-b9ce8a1ef59f.png)
  1. # Server
  2. from socket import *
  3. import subprocess
  4. encodin = 'utf-8'
  5. ip_port = ('127.0.0.1', 1025)
  6. sk = socket(AF_INET, SOCK_STREAM)
  7. sk.bind(ip_port)
  8. sk.listen(5)
  9. while True:
  10. conn, addr = sk.accept()
  11. print("客户端:",addr)
  12. while 1:
  13. cmd = conn.recv(1472)
  14. if not cmd:break
  15. res = subprocess.Popen(cmd.decode('utf-8'),
  16. shell=True,
  17. stdout=subprocess.PIPE,
  18. stderr=subprocess.PIPE)
  19. out = res.stdout.read()
  20. if out:ret = out
  21. else:ret = res.stderr.read()
  22. data_length = str(len(ret)) # 确定长度
  23. conn.send(data_length.encode(encodin)) # 通知对方接收长度
  24. data = conn.recv(1024).decode('utf-8')
  25. if data == 'recv_ready':conn.sendall(ret)
  26. conn.close()
  27. sk.close()
  1. # Client
  2. from socket import *
  3. import time
  4. encodin = 'utf-8'
  5. ip_port = ('127.0.0.1', 1025)
  6. sk = socket(AF_INET, SOCK_STREAM)
  7. sk.connect_ex(ip_port)
  8. try:
  9. while 1:
  10. cmd = input('>>>'.strip())
  11. if len(cmd) == 0:continue
  12. sk.send(cmd.encode(encodin))
  13. length = int(sk.recv(1024).decode(encodin)) # 接收字节长度
  14. sk.send('recv_ready'.encode(encodin)) # 返回确认消息
  15. data = b''
  16. while length > len(data):data += sk.recv(1)
  17. print(data.decode('utf-8'))
  18. finally:
  19. sk.close()

存在问题:
程序的运行速度远远快于网络的传输速度,所以在发送一段数据之前用send去告知该字节长度的方式会放大网络延迟带来的性能损耗


  • 进阶思路

借助struct模块,此模块可以把要传送的数据的长度转换成固定长度的字节,这样接收端在每次接收数据之前只需要先接收这个固定长度的字节看一下,就知道后面发送的数据的长度了,然后开始接收,只要到达这个长度就停止接收,这样就能刚刚好的接收完整的数据.

struct模块
将一个类型转换成固定长度的bytes,比如int类型(int类型可转换的范围:-2147483648 < num < 2147483647)
数据分为有符号和无符号,unsingend代表无符号.
有符号表示的是1个字节,8位,最高位是符号位,一个字节表示的范围:-128~127
无符号表示的是1个字节,8位,所有位都是数值,一个字节表示的范围:0~255
float表示单精度,大部分的操作系统将单精度精确到小数点后7~8位
double表示双精度,大部分操作系统将双精度精确到小数点后15~16位
void指的是无返回值类型,再python中没有这种数据类型
* 表示的是一级指针

![在这里插入图片描述](http://blog.gqylpy.com/media/ai/2019-03/88665f65-f4b1-494c-b43e-b01824df33ec.png)
  1. # Server
  2. import struct, socket, subprocess
  3. ip_port = ('127.0.0.1', 1031)
  4. encod = 'utf-8'
  5. sk = socket.socket()
  6. sk.bind(ip_port)
  7. sk.listen(5)
  8. try:
  9. while 1:
  10. conn, addr = sk.accept()
  11. print("客户端:", addr)
  12. while 1:
  13. cmd = conn.recv(1472).decode(encod)
  14. res = subprocess.Popen(cmd, shell=True,
  15. stdout=subprocess.PIPE,
  16. stderr=subprocess.PIPE)
  17. out = res.stdout.read()
  18. if out:ret = out
  19. else:ret = res.stderr.read()
  20. length_bytes = struct.pack('i', len(ret)) # 将数据长度转换成固定长度的bytes
  21. conn.sendall(length_bytes + ret) # 一次性发送字节长度和数据
  22. finally: # 这句的意思是无论如何都要执行下面的语句,即使是程序崩溃
  23. conn.close()
  24. sk.close()
  1. # Client
  2. import struct, socket, subprocess
  3. ip_port = ('127.0.0.1', 1031)
  4. encod = 'utf-8'
  5. sk = socket.socket()
  6. sk.connect_ex(ip_port)
  7. try:
  8. while 1:
  9. cmd = input('>>>').strip()
  10. if len(cmd) ==0:continue
  11. sk.send(cmd.encode(encod))
  12. length_bytes = sk.recv(4) # 先确定长度
  13. length = struct.unpack('i', length_bytes) # 提取的长度是一个元组(4,)
  14. data = b''
  15. while len(data) < length[0]:data += sk.recv(1) # 再接收数据
  16. print(data.decode(encod))
  17. finally:
  18. sk.close()
发送方 接收方
发送固定长度的bytes + 数据

先接收bytes,以确定数据的长度

再接收数据

使用struct模块,我们可以把要发送的数据的长度转换成固定长度的bytes,然后把这个bytes加在数据的开头一块发送给客户端。客户端先接收固定长度的bytes以确认后面的数据有多长,然后开始一个循环,便可刚刚好的接收完整的数据。



8.socket的更多方法

  • 关于send() 和 sendall()

官方文档对socket模块下的socke.send()和socket.sendall()解释如下:

send() sendall()
返回的值是要发送的字节数量,这个值可能会小于要发送的string的字节数,也就是说可能无法发送string中的所有数据。如果有错误则会抛出异常 尝试发送string的所有数据,成功则放回None,否则抛出异常

  • 其他方法
  1. import socket
  2. s = socket.socket() # 实例化一个套接字对象
  3. # 服务端套接字函数
  4. s.bind() # 绑定(主机,端口号)到套接字
  5. s.listen() # 开始TCP监听
  6. s.accept() # 被动接收TCP客户的连接,(阻塞式)等待连接的到来
  7. # 客户端套接字函数
  8. s.connect_() # 主动初始化TCP服务器连接
  9. s.connect_ex() # 是connect()方法的扩展版本,出错时返回错码,而不是抛出异常
  10. # 公公用途的套接字函数
  11. s.recv() # 接收TCP数据
  12. s.send() # 发送TCP数据
  13. s.sendall() # 发送TCP数据
  14. s.recvfrom() # 接收UDP数据
  15. s.sendto() # 发送UDP数据
  16. s.getpeername() # 连接到当前套接字的远端的地址
  17. s.getsockname() # 当前套接字的地址
  18. s.getsockopt() # 返回指定套接字的参数
  19. s.setsockopt() # 设置指定套接字的参数
  20. s.close() # 关闭套接字
  21. # 面向锁的套接字方法
  22. s.setblocking() # 设置套接字的阻塞与非阻塞模式
  23. s.settimeout() # 设置阻塞套接字操作的超时时间
  24. s.gettimeout() # 得到阻塞套接字操作饿超时时间
  25. # 面向文件的套接字的函数
  26. s.fileno() # 套接字的文件描述符
  27. s.makefile()# 创建一个与该套接字相关的文件

完结

原文: http://blog.gqylpy.com/gqy/227

上一篇:dp协议


下一篇:Web框架本质