应用程序之间通信的直接通道为 socket 编程接口
消息发送时,通过 socket 编程接口将信息交给操作系统,操作系统的 TCP/IP 协议栈驱动将数据交给通讯硬件驱动,最后让通讯硬件将数据将数据发送到网络上。
1. 使用 socket
导入 socket 库(python 预装了 socket 模块哦)
from socket import *
socket 模块中定义了 socket 类和常用的 method 如 bind,listen,connect,accept,recv,send 等方法
函数的描述:
methods | description |
---|---|
socket(AF_INET, SOCK_STREAM) | 实例化 socket 对象,网络层使用 IP 协议,传输层使用 TCP 协议 |
bind((IP, PORT)) | 绑定 IP 地址和端口号,参数为元组 |
recv(BUFLEN) | 从缓冲区读取 BUFLEN 字节 |
send() | 发送数据 (二进制) |
2. server
server 为 client 提供服务,因此需要先处于监听状态,收到客户端请求后创建一个新的 socket 对象,并利用这个对象来读取接受缓冲区的内容以及发送数据。
实例化 socket
参数 AF_INET 表示网络层使用 IP 协议
参数 SOCK_STREAM 表示传输层使用 TCP 协议
IP 为空代表绑定本机所有网络接口 IP 地址
BUFLEN 为允许一次从 socket 缓冲区读取的最多字节数
通过 listen 启动监听,参数 8 代表最多接受多少个等待连接的客户端
from socket import *
IP = ''
PORT = 50000
BUFLEN = 512
listenSocket = socket(AF_INET, SOCK_STREAM)
listenSocket.bind((IP, PORT))
listenSocket.listen(8)
print(f'服务端启动成功,在 {PORT} 端口等待客户端连接...')
接受连接请求
socket 在 listen 之后就处于监听状态,未收到客户端请求时处于阻塞状态,收到客户端的请求连接后,返回一个 socket 对象 dataSocket 和对应的 (IP, PORT) 元组:
dataSocket, addr = listenSocket.accept()
print('接受一个客户端连接:', addr)
运行代码时,当有客户端连接时,打印出对应的 IP 和 PORT:
接受一个客户端连接: (‘127.0.0.1’, 53017)
发送和接收数据
通过 while 循环来持续监听客户端是否发送数据,使用 dataSocket 来读取数据和发送数据,分别使用 recv() 方法和 send() 方法,通过 decode 和 code 函数来进行二进制和字符串之间的编码和解码(发送的数据类型只能是 bytes 类型)。
当客户端没有发送数据是,dataSocket.recv(BUFLEN) 处于阻塞状态,否则返回字节流
当客户端断开连接时,dataSocket.recv(BUFLEN) 返回空字符串
while True:
# 等待接收服务端的消息
# 若未收到消息,则处于阻塞状态等待
received = dataSocket.recv(BUFLEN)
# 如果返回空bytes,表示对方关闭了连接
if not received:
break
# 读取的字节数据是bytes类型,需要解码为字符串
info = received.decode()
print(f'收到消息: {info}')
dataSocket.send(f'服务端收到了消息: {info}'.encode())
关闭 socket
当客户端断开连接时,为了防止程序占用断开,使用 close() 函数关闭 socket
dataSocket.close()
listenSocket.close()
3. client
客户端程序与服务端类似,而且更简洁,包含创建 socket 对象、连接服务端、发送和接收数据以及断开连接几个部分。
实例化 socket
客户端的 PORT 必须与服务端的端口号一致才能连接成功
客户端通过 socket.connect((IP, PORT)) 请求连接服务端
from socket import *
IP = '127.0.0.1'
SERVER_PORT = 50000
BUFLEN = 512
dataSocket = socket(AF_INET, SOCK_STREAM)
请求连接
客户端一切准备就绪后,可以请求与服务端连接
连接的过程中包含了 TCP 的三次握手,客户端首先发送请求,收到服务端的 ACK 后返回一个 ACK,握手成功!
# 连接服务端socket
dataSocket.connect((IP, SERVER_PORT))
发送和接收数据
客户端通过实例化的 socket 来发送和接收数据,发送接收方式和服务端一样
while True:
toSend = input('>>> ')
if toSend =='exit':
break
# 发送消息,也要编码为 bytes
dataSocket.send(toSend.encode())
# 等待接收服务端的消息
# 若未收到消息,则处于阻塞状态等待
received = dataSocket.recv(BUFLEN)
# 如果返回空bytes,表示对方关闭了连接
if not received:
break
# 打印读取的信息
print(received.decode())
dataSocket.close()
断开连接后,关闭 socket
4. cmd
查看占用端口的进程
当运行服务端后,可以在 cmd 输入以下命令查看占用 50000 端口的进程:
netstat -aon | findstr "50000"
-a
表示显示所有的连接和监听端口,-o
表示显示相应系统进程的PID,-n
以数字(IP) 的方式显示地址和端口.
通过 PID 杀死 pid 为 xxxx 的进程
taskkill /F /pid xxxx
启动服务端
只运行服务端时:
>>> netstat -aon | findstr "50000"
TCP 0.0.0.0:50000 0.0.0.0:0 LISTENING 17700
原因是服务端创建了 socket 对象,占用了 50000 端口!
启动客户端
运行服务端后接着运行客户端:
>>> netstat -aon | findstr "50000"
TCP 0.0.0.0:50000 0.0.0.0:0 LISTENING 17700
TCP 127.0.0.1:50000 127.0.0.1:55484 ESTABLISHED 17700
TCP 127.0.0.1:55484 127.0.0.1:50000 ESTABLISHED 15648
增加了两个进程,原因是客户端创建了 socket,连接后服务端又创建了一个 socket 对象!
断开连接
细心的同学会发现,关闭客户端断开连接后还有一个 pid 为 0 的进程,状态为 TIME_WAIT!
>>> netstat -aon|findstr "50000"
TCP 127.0.0.1:55484 127.0.0.1:50000 TIME_WAIT 0
原因如下:
TCP TIME-WAIT 延迟断开TCP 连接时,套接字对被置于一种称为TIME-WAIT 的状态。这样,新的连接不会使用相同的协议、源 IP 地址、目标 IP 地址、源端口和目标端口,直到经过足够长的时间后,确保任何可能被错误路由或延迟的段没有被异常传送。因此,Time_Wait不是多余的状态,而是为了保证通信的正确性、准确性而存在的。因此,这里PID为0的通信均是已“断开”的曾经被进程使用过的连接,而且还没有释放端口。
完整的代码可以在 github 上拉哦~
REFERENCE:
- http://www.python3.vip/tut/py/etc/socket/
- https://www.bilibili.com/video/BV1a7411z75u?p=2
- https://docs.python.org/zh-cn/3/library/socket.html
END