本文介绍socket的基础知识、python中socket模块的使用方法,对于tcp通过自定制报头解决黏包问题。对于udp做一个简单的例子。
socket基础
铺垫
传输层向高层用户屏蔽了下面网络核心的细节(如网络拓扑、所采用的路由选择协议等),它使应用进程看见的就是好像在两个运输层实体之间有一条端到端的逻辑通信信道。
当传输层采用面向连接的 TCP 协议时,尽管下面的网络是不可靠的(只提供尽最大努力服务),但这种逻辑通信信道就相当于一条全双工的可靠信道。
当传输层采用无连接的 UDP 协议时,这种逻辑通信信道是一条不可靠信道。
什么是socket
socket 套接字,它存在于传输层与应用层之间的抽象层,是对底层网络通信的一层抽象,让程序员可以像文件那样操作网络上发送和接收的数据。
- 避免你学习各层的接口,以及协议的使用,socket已经封装好了所有的接口。直接使用这些接口或者方法即可,使用起来方便,提升开发效率。
- Python中socket就是一个模块,通过使用学习模块提供的方法,建立客户端与服务端的通信,建立客户端与服务端的通信,使用方便。
所以,从传输层包括传输层以下,都是操作系统帮助我们封装的各种header,你不用去关心。我们只需要掌握socket这个模块就行。
创建socket对象
创建socket的时候需要指定socket的类型,一般有三种:
- SOCK_STREAM:面向连接的稳定通信,底层是 TCP 协议,参数默认是这个
- SOCK_DGRAM:无连接的通信,底层是 UDP 协议,需要上层的协议来保证可靠性。
- SOCK_RAW:更加灵活的数据控制,能让你指定 IP 头部
还需要指定套接字的家族,有两种
- 基于文件类型的套接字家族:AF_UNIX
- 基于网络类型的套接字家族:AF_INET,最为广泛,本文也将使用这个
基于TCP协议的socket通信
下图是基于TCP协议的socket通信流程
下面通过一个形象的例子来讲解,把上图的整个流程比作打电话。
版本一:
1 | # server.py |
1 | # client.py |
单个客户端与服务端循环通信
上面的版本的最基本的,他只能互发固定的一句话,接下来的版本二实现,两个人的循环通信
版本二:
1 | # server.py |
1 | # client.py |
通信,连接循环
上面这个版本实现了单用户与服务器的循环通信,接下来要实现多用户与服务器的循环通信,其实只是在版本二的基础上增加一层循环。
版本三:
1 | # server.py |
1 | # client.py |
利用socket完成获取远端命令
铺垫
既然想执行远端命令,那我们就需要学习新的模块来实现这个功能,这个模块就是subprocess模块,下面这个例子是完成dir目录查询。
1 | import subprocess |
在版本三的基础上,我们先将上面这个简单的例子做成一个函数
1 | import subprocess |
然后放入版本三中:
1 | # server.py |
1 | # client.py |
但是我们在实际测试中发现,打印的结果并不完整,产生了黏包现象!
小结
tcp创建服务端的四大步骤:
sbla (socket,bind,listen,accept)
黏包现象
黏包现象
现象一: recv端产生的黏包现象
第一次 dir 数据 < 1024
- 服务端产生 508字节 客户端接收508字节
第二次 ipconfig 数据 > 1024
- 服务端产生1455字节 客户端接收1024字节
第三次 dir 数据 < 1024
- 服务端产生508字节 客户端接收431字节
send把数据发送输出缓冲区后,recv进入阻塞状态,recv等待抓取输入缓冲区的数据。
粘包现象的根本原因:缓冲区
———————————加入sleep进行验证———————————
第一次 dir 数据 < 1024
- 服务端产生 508字节 客户端接收508字节
第二次 ipconfig 数据 > 1024
- 服务端产生1455字节 客户端接收1024字节
第三次 dir 数据 < 1024
- sleep(3) 第二次间隔3秒钟接收数据, 发现此数据和之前没有取完的数据黏在一起。
- TCP协议的流式协议,数据与水流一样源源不断,粘包现象
- 服务端产生508字节 客户端接收431字节 + 508字节
- 原因:recv之前,缓冲区已经得到了dir的数据
现象二:send端可能产生的粘包现象(连续send少量数据发到输出缓冲区,可能在缓冲区不断积压,多次写入的数据一次性发到网络,这取决于当前的网络状态)
系统缓冲区
缓冲区一般是8k左右
缓冲区的作用?
没有缓冲区:如果你的网络出现短暂的异常或者波动,你接收数据就会出现短暂的中断,影响你的下载或上传的效率。
就像cpu的缓冲区一样,cpu的效率是特别高的,没有缓冲区,cpu的等待时间就会长了,这样效率就会大大降低。 设计缓冲区也是希望减少recv的等待。
生活上的理解就是输液器的那个小葫芦/蓄水池。
但是缓冲区虽然解决了效率问题,但也带来了粘包问题。
什么是黏包
- 发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据包,合并成一个大的数据包发送(把发送端的缓冲区填满一次性发送)。
- 接收端底层会把tcp段整理排序交给缓冲区,这样接收端应用程序从缓冲区取数据就只能得到整体数据而不知道怎么拆分(tcp协议是流式协议,多条消息之间没有边界)
解决黏包的方案
错误示例:
- 扩大接收recv的上限 recv(10000000),这么多的数据会放在内存。不是解决这个问题的根本原因。
- 故意延长recv的执行时间。 sleep……….. 效率……
如何解决:
recv的工作原理
When no data is available, block untilat least one byte is available or until the remote end is closed.
当缓冲区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭。
When the remote end is closed and all data is read, return the empty string.
关闭远程端并读取所有数据后,返回空字符串。
下面进行验证
1 | # server.py |
1 | # client.py |
核心思路
send可以一次,recv可以多次
目标:发多少,收多少字节
当我第二次给服务器发送命令之前,我应该循环recv直至所有的数据全部取完。
result 3000bytes recv 3次
result 5000bytes recv 5次
result 30000bytes recv ? —-> 循环次数相关
如何限制循环次数?
当你发送的总bytes个数与接收的总bytes个数相等时,循环结束。
如何获取发送的总bytes个数:服务端: len() —> 3400个字节 int
总数据 result = b’sdfjsoidfjoidsjjio’
所以:
服务端要完成:
send(总个数)
send(总数据)
总个数是什么类型? int() send 需要发送bytes类型
将 int 转化成bytes 即可。
方案一:
str(3400) —> bytes(‘3400’) —> b’3400’ —> 4个字节
难点:但是由于总个数不同,头部的字节数是不断变化的
我们需要解决的问题是:无论总字节个数是多少,我们头部是固定的。
需要将不固定长度的 int 转化成一个固定长度的 bytes类型,方便获取头部信息。
struct模块(将一个类型转换成固定长度的bytes,转换后还可以翻转回来)
low版
1 | # server.py |
1 | # client.py |
问题1:较大的数据,直接用struct会报错
1 | import struct |
问题2:报头信息不可能只包含数据总大小,md5,文件名,文件路径。
旗舰版
根据发生的问题自定制报头(总大小,文件名,md5)
我们是在报头的基础上加上一层报头
效果如下
1 | # server.py |
1 | # client.py |
UDP
前面都是基于UDP协议,接下来探讨udp。
当传输层采用无连接的 UDP 协议时,这种逻辑通信信道是一条不可靠信道。 (安全可靠体现在数据能否安全到达。)。面向数据报(无连接)的协议,效率高,速度快。
1 | # server.py |
1 | # client.py |
socketserver
前面我们使用的socket方式是单线程的,接下来使用socketserver开启一个多线程。
模板
1 | # server.py |
1 | # client.py |
刨析
对于一个新出现的模块,要学习它需要有一个入口,实例化对象是一个不错的选择:
1 | # 1.从实例化对象入手 |
1 | # 2.了解它的继承关系 |
1 | # 3.查看它的__init__方法 |
1 | # 4.在BaseServer查看__init__方法 |
1 | # 5.继续查看TCPServer中的__init__方法 |
1 | # 6. 接下来对于 self.server_bind() 找到它的定义 |
1 | # 7. 接下来就到了self.server_activate() 找到它的定义 |
到了这里我们就已经对这个实例化对象有了个大概的了解,它使用了进程方面的模块,也使用了socket模块。
然后查看代码中server.py的 server.serve_forever(),我们想知道到这个方法做了哪些事儿。
1 | class BaseServer: |