文章目录
1. Socket概述
Socket,原意是“插座”,在计算机通信领域,Socket一般被翻译为“套接字”。百度百科关于Socket的定义如下:“所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口” [1]。
如何理解?为理解上面这段关于Socket的定义,我们需要先简单了解一下TCP/IP协议。TCP/IP 协议,即传输控制协议/网际协议(Transmission Control Protocol/Internet Protocol),是指能够在多个不同网络间实现信息传输的协议簇,其是一系列网络通信协议的总称 [2]。通俗点理解,TCP/IP协议是对计算机之间通信所必须遵守的规则的描述,只有遵守这些规则,计算机之间才能进行通信 [3]。TCP/IP 协议采用4层结构,分别是应用层、运输层、网络层和链路层,如下图1所示 [1] [4]:
- 应用层:应用层负责处理特定的应用程序细节。应用层的协议主要有Telnet、FTP、SMTP等。
- 运输层:运输层也称为传输层,其主要负责提供应用程序之间的通信。运输层的协议主要有TCP和用户数据报协议(User Datagram Protocol,UDP)。
- 网络层:网络层也称为互联网层,主要负责相邻计算机之间的通信。网络层的协议主要有IP、ICMP、IGMP。
- 链路层:链路层也称为数据链路层或网络接口层,其通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。链路层主要负责为IP模块发送和接收IP数据报,为ARP模块发送ARP请求和接收ARP应答,以及为RARP发送RARP请求和接收RARP应答。链路层的协议主要有ARP、RARP。
而Socket就是位于应用层与运输层中间的软件抽象层,如下图2所示。Socket是对TCP/IP协议的封装 [5]。Socket提供了一组接口,以用于应用程序与网络协议栈进行交互。通过Socket,应用程序可以方便地与网络协议栈进行交互,从而不同应用程序可以进行通信。
因此,我们可以将Socket抽象为应用程序间进行通信的端点,如下图3所示。
更通俗点理解,就像我们把插头插到插座上就能从电网中获得电力供应一样,应用程序需要先连接到因特网才能接收或发送数据,而 Socket 就是用来将应用程序连接到因特网的工具 [6]。
Socket典型的应用就是Web服务器和浏览器:浏览器获取用户输入的URL,向服务器发起请求;服务器分析接收到的URL,将对应的网页内容返回给浏览器;浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户 [6]。
2. Socket创建
Python中,我们用socket()函数来创建套接字,其语法格式如下(需先import socket模块)[7]:
socket.socket([family[, type[, proto]]]) # 使用给定的地址族、套接字类型及协议号来创建套接字
- family: 地址族,该参数默认为socket.AF_INET。
- type: 套接字类型,该参数默认为socket.SOCK_STREAM。
- protocol: 协议号,一般默认为 0,这样系统就会根据地址族和套接字类型,自动选择合适的协议。
3. Socket对象内建方法
这一部分,我们只介绍几个下面我们会用到的重要方法,其余方法请参考文档 [10]。
方法 | 描述 |
---|---|
服务器方法 | |
socket.bind(address) | 将套接字绑定到地址address,地址的格式取决于上述地址族。对于AF_INET而言,以元组(host, port)的形式表示地址。 |
socket.listen([backlog]) | 允许服务器接受连接,即监听连接。backlog指定在拒绝连接之前,系统可以挂起的最大连接数量。该值至少为0,大部分应用程序设为5就可以了。 |
socket.accept() | 接受连接。Socket必须先绑定到一个地址并监听连接。 该方法返回一对(conn, address),其中conn是一个新的Socket对象,用于在连接上发送和接收数据;address是绑定到连接另一端Socket的地址。 |
客户端方法 | |
socket.connect(address) | 连接到address处的套接字。地址的格式取决于上述地址族。 |
socket.connect_ex(address) | connect方法的扩展版本。不同的是,该方法在出错时会返回一个错误指示符,而不是像connect在出错时抛出异常。如果该方法成功,那么错误指示符为0;否则,错误指示符为变量errno的值。该方法对于支持例如异步连接很有用。 |
公共方法 | |
socket.recv(bufsize[, flags]) | 接收数据,返回值是一个字节对象,表示接收到的数据。bufsize指定了一次所能接收的最大数据量。 |
socket.send(bytes[, flags]) | 发送数据,该Socket必须连接到一个远程Socket。 该方法返回发送的字节数,该值可能小于bytes的字节数。 |
socket.sendall(bytes[, flags]) | 发送数据,该Socket必须连接到一个远程Socket,成功则返回None。不同于send方法,该方法会一直从bytes中发送数据,直到所有数据都已发送,或发生错误。 |
socket.recvfrom(bufsize[, flags]) | 接收数据,返回值是一对(bytes, address),其中bytes是表示接收到的数据的字节对象,而address是发送数据的Socket的地址。 |
socket.sendto(bytes, flags, address) | 发送数据,该Socket不能连接到一个远程Socket,因为address指定了目标Socket。该方法返回发送的字节数。 |
socket.close() | 关闭Socket。 |
4. Socket编程一般思路
4.1 基于TCP的Socket编程的一般思路
服务器 [11]:
- 创建服务器Socket:socket.socket(type=socket.SOCK_STREAM)
- 绑定创建的服务器Socket到一个ip和端口:socket.bind()
- 监听客户端的连接请求:socket.listen()
- 接受客户端的连接请求:socket.accept()
- 接收客户端传来的数据,或发送数据给客户端:socket.recv() , 或socket.send()/socket.sendall()
- 关闭连接:socket.close()
客户端 [11]:
- 创建客户端Socket:socket.socket(type=socket.SOCK_STREAM)
- 连接客户端Socket到服务器ip和端口:socket.connect(),socket.connect_ex()
- 接收服务器传来的数据,或发送数据给服务器:socket.recv() , 或socket.send()/socket.sendall()
- 关闭连接:socket.close()
上述流程可以总结如下图4所示:
我们以日常打电话为例,先形象直观地理解一下上述基于TCP的Socket编程的一般思路 [6]。感兴趣的,可以再深入了解一下TCP协议。
服务器——相当于接电话方:
- 创建服务器Socket:相当于根据网络制式买个电话/手机,座机、5G手机还是卫星电话?
- 绑定创建的服务器Socket到一个ip和端口:相当于给电话/手机申请个号码,通过这个号码就能联系到指定的人。
- 监听客户端的连接请求:相当于给电话/手机连上网络等待别人打电话。
- 接受客户端的连接请求:相当于接电话,这样一路通话连接就建立了。
- 接收客户端传来的数据,或发送数据给客户端:相当于讲电话,可以双向通话。
- 关闭连接:相当于挂断电话或关闭电话/手机。
客户端——相当于打电话方:
- 创建客户端Socket:相当于根据网络制式买个电话/手机,座机、5G手机还是卫星电话?
- 连接客户端Socket到服务器ip和端口:相当于拨打电话,得输入对方的号码。
- 接收服务器传来的数据,或发送数据给服务器:相当于讲电话,可以双向通话。
- 关闭连接:相当于挂断电话或关闭电话/手机。
就像我们日常打电话需要通话双方先建立连接之后才能讲话一样,服务器需要使用socket.listen()、socket.accept()方法,客户端需要使用socket.connect()/socket.connect_ex()方法先彼此建立连接,之后才能进行网络通信。
注释:
- 网络中的主机可以由“ip地址”唯一标识,而主机上的应用程序又可以由“协议+端口”唯一标识 [12]。绑定Socket到一个ip和端口,这样对应的应用程序就可以通过Socket进行网络通信了。
- socket.listen([backlog])方法中backlog参数指定了在拒绝连接之前,系统可以挂起的最大连接数量。这和我们日常打电话类似,如果我们同时或者在通话过程中又接到了其他电话,那么这些电话可以被挂起,等待直到被接听。
下面,我们以下述代码为例,再深入理解一下上述基于TCP的Socket编程的一般思路。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
""" 服务器 """
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建服务器Socket
s.bind(('127.0.0.1', 8888)) # 绑定创建的服务器Socket到一个ip和端口
s.listen(5) # 监听客户端的连接请求
print("Waiting connection from client...")
conn, address = s.accept() # 接受客户端的连接请求
print("Connected by {}".format(address)) # 打印客户端Socket的地址
receive_message = conn.recv(1024).decode() # 接收客户端传来的数据并解码
print("Message received: {}".format(receive_message)) # 打印客户端传来的数据
send_message = receive_message.upper().encode() # 将客户端传来的字符串转为大写之后,再编码发送回客户端
conn.send(send_message) # 发送数据给客户端
conn.close() # 关闭连接
s.close()
#!/usr/bin/env python
# -*- coding:utf-8 -*-
""" 客户端 """
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建客户端Socket
s.connect(('127.0.0.1', 8888)) # 连接客户端Socket到服务器ip和端口
print(s.getsockname()) # 打印客户端Socket的地址
send_message = 'Hello, world!'
s.send(send_message.encode()) # 发送数据给服务器
receive_message = s.recv(1024).decode() # 接收服务器传来的数据并解码
print("Message received: {}".format(receive_message)) # 打印服务器传来的数据
s.close() # 关闭连接
先运行服务器脚本,再运行客户端脚本,结果如下:
""" 服务器 """
Waiting connection from client...
Connected by ('127.0.0.1', 52161)
Message received: Hello, world!
""" 客户端 """
('127.0.0.1', 52161)
Message received: HELLO, WORLD!
这里需要注意以下几点:
一、我们使用的是socket.accept()方法返回的新的Socket(称为服务Socket或连接Socket)与客户端进行网络通信的,而非服务器Socket(称为监听Socket)本身。关于监听Socket和服务Socket/连接Socket,具体请参考[13]、[14]。
二、Python3以后,socket传递的都是bytes类型的数据,字符串需要先转换一下,string.encode()即可;另一端接收到的bytes数据想转换成字符串,只要bytes.decode()一下就可以 [11]。
4.2 基于UDP的Socket编程的一般思路
相比基于TCP的Socket编程,基于UDP的Socket编程更加简单。由于UDP没有TCP的握手和挥手的过程,因此socket.listen()、socket.accept()和socket.connect()/socket.connect_ex()方法都不需要。
服务器:
- 创建服务器Socket:socket.socket(type=socket.SOCK_DGRAM)
- 绑定创建的服务器Socket到一个ip和端口:socket.bind()
- 接收客户端传来的数据,或发送数据给客户端:socket.recvfrom() , 或socket.sendto()
- 关闭连接:socket.close()
客户端:
- 创建客户端Socket:socket.socket(type=socket.SOCK_DGRAM)
- 接收服务器传来的数据,或发送数据给服务器:socket.recvfrom() , 或socket.sendto()
- 关闭连接:socket.close()
上述流程以及和基于TCP的Socket编程的区别可以总结如下图5所示:
我们先还是以日常发短信为例,先形象直观地理解一下上述基于UDP的Socket编程的一般思路。感兴趣的,可以再深入了解一下UDP协议。
服务器——相当于收短信方:
- 创建服务器Socket:相当于根据网络制式买个手机,3G手机、4G手机还是5G手机?
- 绑定创建的服务器Socket到一个ip和端口:相当于买了张电话卡,通过这个号码就能联系到指定的人。
- 接收客户端传来的数据,或发送数据给客户端:相当于收发短信。
- 关闭连接:相当于关闭手机。
客户端——相当于发短信方:
- 创建客户端Socket:相当于根据网络制式买个手机,3G手机、4G手机还是5G手机?
- 接收服务器传来的数据,或发送数据给服务器:相当于收发短信。
- 关闭连接:相当于关闭手机。
和打电话不同,我们日常收发短信,并不需要收发短信双方先建立连接,而是一方发短信给另一方,另一方收到短信之后再回过去。客户端Socket通过socket.sendto()方法通过指定address参数的形式将消息发送给对应地址的服务器Socket,这就相当于发短信时需要输入对方的手机号。然后服务器Socket通过socket.recvfrom()方法接收消息,并返回对方的地址,这就相当于我们在收到别人发给我们的短信的时候,手机不仅会显示对方发来的短信内容,还会显示对方的手机号码。这样,服务器Socket就又可以通过socket.sendto()方法通过指定address参数的形式给客户端发送消息,这就相当于给对方手机号回消息。
下面,我们以下述代码为例,再深入理解一下上述基于UDP的Socket编程的一般思路。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
""" 服务器 """
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 创建服务器Socket
s.bind(('127.0.0.1', 8888)) # 绑定创建的服务器Socket到一个ip和端口
receive_message, address = s.recvfrom(1024) # 接收客户端传来的数据
print("Receive message from {}".format(address)) # 打印客户端Socket的地址
receive_message = receive_message.decode() # 解码客户端传来的数据
print("Message received: {}".format(receive_message)) # 打印客户端传来的数据
send_message = receive_message.upper().encode() # 将客户端传来的字符串转为大写之后,再编码发送回客户端
s.sendto(send_message, address) # 发送数据给客户端
s.close() # 关闭连接
#!/usr/bin/env python
# -*- coding:utf-8 -*-
""" 客户端 """
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 创建客户端Socket
send_message = 'Hello, world!'
s.sendto(send_message.encode(), ('127.0.0.1', 8888)) # 发送数据给服务器
print(s.getsockname()) # 打印客户端Socket的地址
receive_message, address = s.recvfrom(1024) # 接收服务器传来的数据
print("Receive message from {}".format(address)) # 打印服务器Socket的地址
receive_message = receive_message.decode() # 解码服务器传来的数据
print("Message received: {}".format(receive_message)) # 打印服务器传来的数据
s.close() # 关闭连接
先运行服务器脚本,再运行客户端脚本,结果如下:
""" 服务器 """
Receive message from ('127.0.0.1', 64410)
Message received: Hello, world!
""" 客户端 """
('0.0.0.0', 64410)
Receive message from ('127.0.0.1', 8888)
Message received: HELLO, WORLD!