Python实践Socket通信原理
进程之间如何实现网络通信
进程通信的概念最初来源于单机系统,由于每个进程都在自己的地址范围内运行,为了保证两个相互通信的进程之间既互不干扰又能协调一致地工作,操作系统为进程通信提供了相应的措施,如:
Unix BSD有管道(pipe)、命名管道(named pipe)、软中断信号(signal),Unix system V有消息(message)、共享存储区(shared memory)和信号量(semaphore)等。
但是这些仅限于本机进程之间的通信,进程之间的网络通信要解决不同主机进程之间相互通信的问题,为此,首先要解决的是网络中进程标识的问题。同一主机上,不同进程可以用进程号(process ID)唯一标识,但在网络环境下,各主机独立分配的进程号并不能唯一标识该进程。例如,主机A赋予某进程ID为2363,主机B中也可能存在2363号进程,因此单靠进程ID也就失去了标识的意义。其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同,因此,进程之间的网络通信还要解决多重协议的识别问题。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层的IP地址可以唯一标识网络中的主机,而传输层的协议+端口可以唯一标识主机中的应用程序(进程),这样利用三元组(协议,IP地址,端口)就可以标识网络的进程了。使用TCP/IP协议的应用程序通常采用Unix BSD的Socket来实现进程之间的网络通信。
Socket通信过程
Socket起源于Unix,通常翻译为“套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socket是在应用层和传输层(TCP/IP协议族)之间的一个软件抽象层,它把TCP/IP层复杂的操作抽象成一组简单的接口,供应用层调用,从而实现进程在网络中通信。
socket抽象层在网络中的位置:
TCP通信的标准流程一般是,服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后主动连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
Socket通信过程:
Socket服务端
开发步骤
- 创建服务端端套接字对象
- 绑定端口号
- 设置监听
- 等待接受客户端的连接请求
- 接收数据
- 发送数据
- 关闭套接字
import socket
if __name__ == '__main__':
# 1.创建tcp服务端套接字对象
# socket()函数的参数说明:
# AF_INET:协议族参数,AF_INET决定了socket的地址类型为ipv4地址(32位的)与端口号(16位的)的组合
# SOCK_STREAM: 套接字类型参数,SOCK_STREAM决定了socket接受数据格式为stream(流)
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让服务端程序退出后,端口号立即释放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 2.给程序绑定端口号
tcp_server_socket.bind(("", 8989))
# 3.设置监听
# 128表示最大等待建立连接的个数
# listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字来完成
tcp_server_socket.listen(128)
# 4.等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
# 专门和客户端通信的套接字: service_client_socket
# 客户端的ip地址和端口号: ip_port
service_client_socket, ip_port = tcp_server_socket.accept()
# 代码执行到此说明连接建立成功
print("客户端的ip地址和端口号:", ip_port)
# 5.接收客户端发送的数据, 这次接收数据的最大字节数是1024
recv_data = service_client_socket.recv(1024)
# 获取数据的长度
recv_data_length = len(recv_data)
print("接收数据的长度为:", recv_data_length)
# 对二进制数据进行解码
recv_content = recv_data.decode("gbk")
print("接收客户端的数据为:", recv_content)
# 准备发送的数据
send_data = "ok".encode("gbk")
# 6.发送数据给客户端
service_client_socket.send(send_data)
# 关闭服务与客户端的套接字, 终止和客户端通信的服务
service_client_socket.close()
# 7.关闭服务端的套接字, 终止和客户端提供建立连接请求的服务
tcp_server_socket.close()
执行结果
客户端的ip地址和端口号: ('172.16.47.209', 52472)
接收数据的长度为: 5
接收客户端的数据为: hello
Socket客户端
开发步骤
- 创建客户端套接字对象
- 和服务端套接字建立连接
- 发送数据
- 接收数据
- 关闭客户端套接字
import socket
if __name__ == '__main__':
# 1.创建tcp客户端套接字对象
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.和服务端应用程序建立连接
tcp_client_socket.connect(("192.168.131.62", 8080))
# 代码执行到此,说明连接建立成功
# 准备发送的数据
send_data = "hello".encode("gbk")
# 3.发送数据
tcp_client_socket.send(send_data)
# 4.接收数据, 这次接收的数据最大字节数是1024
recv_data = tcp_client_socket.recv(1024)
# 返回的直接是服务端程序发送的二进制数据
print(recv_data)
# 对数据进行解码
recv_content = recv_data.decode("gbk")
print("接收服务端的数据为:", recv_content)
# 5.关闭套接字
tcp_client_socket.close()
执行结果
b'ok'
接收服务端的数据为: ok
send和recv原理
当创建一个socket对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存分配的一片空间。
不管是send还是recv都不是直接发送数据到对方和接收到对方的数据,发送数据会写入到发送缓冲区,接收数据是从接收缓冲区来读取,发送数据和接收数据最终是由操作系统控制网卡来完成。
send和recv原理剖析图:
TCP三次握手和四次挥手的Socket过程
TCP三次握手
- 服务端调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待;
- 客户端Socket对象调用connect()向服务端发送了一个SYN报文并阻塞;
- 服务端完成了第一次握手,即发送SYN+ACK报文应答;
- 客户端收到服务端发送的应答报文,从connect()返回,再发送一个ACK报文给服务器,完成第二次握手,该应答报文可携带客户端到服务端的数据;
- 服务端Socket对象接收客户端第三次握手ACK确认,此时服务端从accept()返回,建立连接。
TCP四次挥手
- 某个应用进程调用close()主动关闭,发送一个FIN;
- 另一端接收到FIN后被动执行关闭,并发送ACK确认;
- 之后被动执行关闭的应用进程调用close()关闭Socket,并也发送了一个FIN;
- 接收到这个FIN的一端向另一端ACK确认。