网络通信入门 - 套接字
套接字
介绍
套接字(socket)在网络编程中是最低级的,使用套接字可以获得最大的自由度。在实际进行网络编程时,比如进行爬虫,我们会使用 requests 库,来更快的进行 HTTP 请求(体现封装的概念)。
什么是套接字?
要理解套接字是什么,我们先看它的英文单词释义。
套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的端点。socket 取插座义,把服务器和客户端看作是插头和插座,是接入点和被接入点的关系。因此,socket 可以用来进行收发消息。
使用
我们先来认识 python 中套接字的支持。python 中,套接字由内置库 socket 提供。
建立套接字
使用 socket.socket(socket_family,socket_type)
函数建立套接字。
socket.socket
函数接收两个参数,分别为协议簇和套接字类型。
在本次(以及将来很久),你都只需要掌握一种套接字。其协议簇为 socket.AF_INET
,指代 IPv4 协议,套接字类型为 socket.SOCK_STREAM
,即基于 TCP 协议建立套接字。
以下是建立一个套接字的示例代码:
1 |
|
接下里的步骤略有点复杂,这是一张流程图。我们会分客户端和服务器端来分别讲解。
服务器端
服务器端在进入消息循环前分三步。1)绑定地址 2)开始监听 3)等待客户端连接
这三步分别通过 socket 对象的三个方法实现。三个方法使用方法如下表。(注意:下表中的 socket.
并不是指 socket 内置库,而是 socket 对象)
方法名 | 使用方法 |
---|---|
socket.bind((host,port)) |
接收一个二元组。host 为 IP地址,例:127.0.0.1 ;port 指端口。端口号用来标记每个主机的多个网络连接。取值范围为 0~65535。(有些端口通常是为特定服务保留的,例如:80-HTTP,443-HTTPS,22-SSH,所以一般使用四位或五位的端口号) |
socket.listen(backlog) |
接收一个整数。表示保留的还未连接的客户端数。一般设为 5(即保留 5 个还未成功连接的客户端) |
socket.accept() -> (conn, addr) |
阻塞等待,直到有客户端连接。返回一个二元组。第一个为客户端的套接字,第二个为客户端的地址。 |
1 |
|
客户端
客户端就比较简单,只需要进行连接。
方法名 | 使用方法 |
---|---|
socket.connect((host,port)) |
接收一个二元组。格式与 socket.bind 相同。连接到该地址的服务器。 |
1 |
|
消息循环
下一步,就是让服务器端和客户端实现通信了!
为此,我们再引入两个方法。
方法名 | 使用方法 |
---|---|
socket.send(bytes) |
发送二进制数据给套接字。本套接字必须已连接到远程套接字。 |
socket.recv(bufsize) |
从套接字接收数据。返回值是一个字节对象,表示接收到的数据。bufsize 指定一次接收的最大数据量。 |
你可能注意到了上面粗体的两个词。事实上,你并不能发送一个字符串。因为字符串并不是通用的(有各种各样的编码),你能发送的只有二进制数据。那么,怎么编码、解码二进制数据呢?python 为我们提供了两个方法。
方法名 | 使用方法 |
---|---|
str.encode(encoding) |
将字符串编码为 bytes 对象。编码由 encoding 指定(请使用 utf-8 编码,因为是国际通用,而不是其他编码) |
bytes.decode(encoding) |
将 bytes 对象解码为字符串。编码由 encoding 指定。 |
来试一试吧!
1 |
|
1 |
|
保证报文完整性
分包/沾包
分包
分包现象就是一次传输的数据过多时,TCP协议会自动将本次的数据拆分成多个消息包进行发送。我们来做一个实验。传输一个比较大的文件。如果你身边没有比较好的文件,这里有一个代码来生成一个大约 5M 的文件。
1 |
|
提示:open(filename,"rb")
可打开一个二进制模式的只读文件。此时调用 read
方法就会返回 bytes 对象。
可以看到,无论你的 bufsize
设置的是多大,TCP 协议会自动将本次的数据拆分成多个消息包进行发送。
粘包
粘包现象就是当网络繁忙时,TCP协议会将多份小的消息包打包成一个消息包进行发送。例如,发送方发送两个字符串 hello
+ world
,接收方却一次性接收到了 helloworld
。
自定义协议
为解决这个问题,我们可以自己定义一个通信协议。起名为 SJCP (Simple Jianping Communication Protocal)。
我们来写两个函数,来处理发送和接收。
发送
发送相比接收更简单,我们先来解决发送的问题。我们要解决的问题有:1)计算出消息体长度 2)将消息头的内容添加到消息体前面
定义一个函数即可。
1 |
|
来解释一下:
- bytes 对象可以想象成字符串,支持加法、
len
、切片等操作。(这是多态的体现) - int 存在方法
int.to_bytes(length, byteorder)
。length
指定转化为bytes
的长度,byteorder
指定转化后是大端序还是小端序。(我选择的是小端序,其实都可以,我们到解码的时候就会知道这个顺序怎么使用)
接收
接收就比较复杂了,我们画一个流程图梳理一下。
1 |
|
我们可以看到,int 也有 from_bytes
方法,little
与前面的 little
相对应。
预告
在下一次,我们会把我们今天写的代码全部封装起来,并讲解类和面向对象。那时就会感受到封装带来的好处。
应用
请编写一个传输文件的发送端和接收端,并尝试在两台电脑上,真正实现文件的异地传输。要求:能够将文件名一并传输(请在SJCP的基础上修改消息体内容,或者你也可以设立你自己的协议)。