网络编程
套接字
在任何类型的通信开始之前,网络应用程序必须创建套接字。可以将他们比作电话插孔,没有它将无法进行通信。
套接字的起源可以追溯到 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 模块函数
| 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 地址
| 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
"""
|