Skip to content

网络编程

套接字

在任何类型的通信开始之前,网络应用程序必须创建套接字。可以将他们比作电话插孔,没有它将无法进行通信。

套接字的起源可以追溯到 20 世纪 70 年代,它是加利福尼亚大学的伯克利版本 UNIX 的一部分。套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信(Inter Process Communication, IPC)。有两种类型的套接字:基于文件的和面向网络的。

UNIX 套接字是我们所讲的套接字的第一个家族,并且拥有一个“家族名字” AF_UNIX(又名 AF_LOCAL),它代表地址家族(address family): UNIX。

因为两个进程运行在同一台计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持它们的底层基础结构。这是能够说得通的,因为文件系统是一个运行在同一主机上的多个进程之间的共享常量。

第二种类型的套接字是基于网络的,它也有自己的家族名字 AF_INET,或者地址家族:因特网。

在所有的地址家族之中,目前 AF_INET 是使用得最广泛的。

套接字地址:主机-端口对

如果一个套接字像一个电话插孔--允许通信的一些基础设施,那么主机名和端口号就像区号和电话号码的组合。然而,拥有和通信能力本身并没有任何好处,除非你知道电话打给谁以及如何拨打电话。一个网络地址由主机名和端口号对组成,而这是网络通信所需要的。此外,并未事先说明必须有其他人在另一端接听;否则,你将听到这个熟悉的声音:“对不起,您所拨打的电话是空号,请核对后再拨”。你可能已经在浏览网页的过程中见过一个网络类比,例如“无法连接服务器,服务器没有响应或者服务器不可达。”

面向连接的套接字

面向连接的通信提供序列化的、可靠的和不重复的数据交付,而没有记录边界。这基本上意味着每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序

实现这种连接类型的主要协议是传输控制协议(更为人熟知的是它的缩写 TCP)。为了创建 TCP 套接字,必须使用 SOCK_STREAM 作为套接字类型。TCP 套接字的名字 SOCK_STREAM 基于流套接字的其中一种表示。因为这些套接字(AF_INET)的网络版本使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合这两种协议(TCP 和 IP)来进行(当然,也可以使用 TCP 和本地[非网络的 AF_LOCAL/AF_UNIX]套接字,但是很明显此时并没有使用 IP)

socket 模块函数

1
2
3
4
5
6
socket.socket(
    family=<AddressFamily.AF_INET: 2>,
    type=<SocketKind.SOCK_STREAM: 1>,
    proto=0,
    fileno=None,
)
名称 描述
服务器套接字方法 s.bind() 将地址(主机名、端口号对)绑定到套接字上
s.listen() 设置并启动 TCP 监听器
s.accept() 被动接受 TCP 客户端连接,一直等待直到连接到达(阻塞)
客户端套接字方法 s.connect() 主动发起 TCP 服务器连接
普通的套接字方法 s.recv() 接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量
s.send() 发送 TCP 数据,将 string 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 string 的字节大小
s.sendall() 完整的发送 TCP 数据。将 string 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回 None,失败则抛出异常
s.close() 关闭套接字

获取百度的 ip 地址

1
2
3
4
5
6
7
8
import socket

hostname = "www.baidu.com"
hostip = socket.gethostbyname(hostname)
print(hostip)
"""
182.61.200.7
"""

创建 TCP 服务器

如果想完成一个 TCP 服务器的功能,需要的流程如下:

  • 使用 socket() 创建一个套接字
  • 使用 bind() 绑定 IP 和 port
  • 使用 listen() 使套接字变为可被动连接
  • 使用 accept() 等待客户端的连接
  • 使用 recv/send() 接收发送数据

例如,使用 socket 模块,通过客户端浏览器向本地服务器(IP 地址为 127.0.0.1)发送请求,服务器接到请求,向浏览器发送“Hello World”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import socket
host = "127.0.0.1" # 主机 IP
port = 8080 # 端口号
web = socket.socket() # 创建 socket 对象
web.bind((host, port)) # 将地址绑定到 socket 上
web.listen(5) # 设置最多连接数
print("服务器等待客户端连接...")
while True:
    conn, addr = web.accept() # 建立客户端连接
    data = conn.recv(1024) # 获取客户端请求数据
    print(data)
    conn.sendall(b"HTTP/1.1 200 OK\r\n\r\nHello World") # 向客户端发送数据
    conn.close()
"""
服务器等待客户端连接...
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n\r\n'
b'GET /favicon.ico HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: keep-alive\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36\r\nAccept: image/webp,image/apng,image/*,*/*;q=0.8\r\nReferer: http://127.0.0.1:8080/\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n\r\n'
"""

运行后打开谷歌浏览器,输入网址: 127.0.0.1:8080,成功连接服务器以后,浏览器显示“Hello World”

创建 TCP 客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import socket
s = socket.socket()
host = "127.0.0.1"
port = 8080
s.connect((host, port))
send_data = input("请输入要发送的数据:")
s.send(send_data.encode())
recv_data = s.recv(1024).decode()
print("接收到的数据为:", recv_data)
s.close()
"""
请输入要发送的数据:hi
接收到的数据为: HTTP/1.1 200 OK

Hello World
"""
"""
服务器等待客户端连接...
b'hi'
"""

执行 TCP 服务器和客户端

既然客户端和服务端可以使用 Socket 进行通信,那么,客户端就可以向服务器发送文字,服务器接到信息后,显示消息内容并且输入文字返回给客户端。客户端接收到响应,显示该文字,然后继续向服务器发送消息。这样,就可以实现一个简单的聊天窗口。当有一方输入“byebye”时,则退出系统中断聊天

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# server.py
import socket
host = "0.0.0.0"
port = 12345
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((host, port))
s.listen(1)
sock, addr = s.accept()
print("连接已经建立")
info = sock.recv(1024).decode()
while info != "byebye":
    if info:
        print(f"接收到的内容:{info}")
    send_data = input("请输入发送内容:")
    sock.send(send_data.encode())
    if send_data == "byebye":
        break
    info = sock.recv(1024).decode()
sock.close()
s.close()
"""
连接已经建立
接收到的内容:村里终于通网了23333
请输入发送内容:hhh    
接收到的内容:bye
请输入发送内容:byebye
"""
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import socket
s = socket.socket()
host = "127.0.0.1"
port = 12345
s.connect((host, port))
print("已连接")
info = ""
while info != "byebye":
    send_data = input("请输入要发送的数据:")
    s.send(send_data.encode())
    if send_data == "byebye":
        break
    info = recv_data = s.recv(1024).decode()
    print("接收到的内容:", recv_data)
s.close()
"""
已连接
请输入要发送的数据:村里终于通网了23333
接收到的内容: hhh
请输入要发送的数据:bye   
接收到的内容: byebye
"""

多个用户请求的服务器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from socket import socket
from base64 import b64encode
from json import dumps
from threading import Thread
def main():
    class FileTransferHandler(Thread):
        def __init__(self, cclient):
            super().__init__()
            self.cclient = cclient
        def run(self):
            my_dict = {}
            my_dict['filename'] = 'fine_view.jpg'
            # JSON是纯文本不能携带二进制数据
            # 所以图片的二进制数据要处理成base64编码
            my_dict['filedata'] = data
            json_str = dumps(my_dict)
            self.cclient.send(json_str.encode('utf-8'))
            self.cclient.close()
    server = socket()
    server.bind(("0.0.0.0", 8080))
    server.listen(512)
    print('服务器启动开始监听...')
    with open("fine_view.jpg", "rb") as f:
        # 将二进制数据处理成base64再解码成字符串
        data = b64encode(f.read()).decode('utf-8')
    while True:
        client, addr = server.accept()
        # 启动一个线程来处理客户端的请求
        FileTransferHandler(client).start()
if __name__ == '__main__':
    main()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os
from socket import socket
from json import loads
from base64 import b64decode
def main():
    client = socket()
    client.connect(("127.0.0.1", 8080))
    print("已连接")
    # 定义一个保存二进制数据的对象
    in_data = bytes()
    # 由于不知道服务器发送的数据有多大每次接收1024字节
    data = client.recv(1024)
    while data:
        # 将收到的数据拼接起来
        in_data += data
        data = client.recv(1024)
    # 将收到的二进制数据解码成JSON字符串并转换成字典
    my_dict = loads(in_data.decode('utf-8'))
    filename = my_dict['filename']
    filedata = my_dict['filedata'].encode('utf-8')
    with open(f"{os.getcwd()}/{filename}", 'wb') as f:
        # 将base64格式的数据解码成二进制数据并写入文件
        f.write(b64decode(filedata))
    print('图片已保存.')
if __name__ == '__main__':
    main()

UDP 编程

UDP 是面向消息的协议,如果通信时不需要建立连接,数据的传输自然是不可靠的。UDP 一般用于多点通信和实时的数据业务,例如:

  • 语音广播
  • 视频
  • 聊天软件
  • TFTP(简单文件传输)
  • SNMP(简单网络管理协议)
  • RIP(路由信息协议,如报告股票市场、航空信息)
  • DNS(域名解释)

和 TCP 类似,使用 UDP 的通信双方也分为客户端和服务端

UDP 服务器不需要 TCP 服务器那么多设置,因为它们不是面向连接的。除了等待传入的连接之外,几乎不需要做其他工作。下面我们来实现一个将摄氏温度转化为华氏温度的功能

例如,在客户端输入要转换的摄氏温度,然后发送给服务器,服务器根据转化公式,将摄氏温度转化为华氏温度,发送给客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
"""
服务端
"""
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 8888))
data, addr = s.recvfrom(1024)
print("Received from %s:%s" % addr)
data = float(data) * 1.8 + 32
send_data = f"转换后的温度(单位:华氏温度): {data}"
s.sendto(send_data.encode(), addr)
s.close()
"""
Received from 127.0.0.1:53498
"""

使用 socket.socket() 函数创建套接字,其中设置参数为 socket.SOCK_DGRAM,表名创建的是 UDP 套接字。
此外需要注意,s.recvfrom() 函数生成的 data 数据类型是 byte,不能直接进行四则运算,需要将其转化为 float 浮点型数据。
最后在使用 sendto() 函数发送数据时,发送的数据必须是 byte 类型,所以需要使用 encode() 函数将字符串转化为 byte 类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 8888))
data, addr = s.recvfrom(1024)
print("Received from %s:%s" % addr)
data = float(data) * 1.8 + 32
send_data = f"转换后的温度(单位:华氏温度): {data}"
s.sendto(send_data.encode(), addr)
s.close()
"""
请输入要转换的温度(单位: 摄氏度): 36
转换后的温度(单位:华氏温度): 96.8
"""