加密通信 实验
作业罢了。
实验内容
-
学习理解网络通信
-
学习数据加密和解密
-
开发网络通信的客户端程序、服务器端程序,实现客户端将传输的网络数据进行加密、服务器端将收到的加密数据进行解密,在客户端和服务器端间传输加密数据。需要对比发送的数据、接受的数据是否一致,验证网络传输的正确性;需要对比两端的明文数据、密文数据是否一致,验证加密、解密的正确性。
本文使用套接字 + 多线程
的方式完成服务器通信,编程语言为Python
。
本文使用TEA加密
算法进行加解密。
Socket
Socket 是一种网络通信的基础机制,用于在不同计算机之间传输数据。Socket 提供了一个在网络上进行数据通信的端点,通过它可以实现客户端与服务器之间的通信。
Socket 就像一个电话插孔(“插座”),它允许一台计算机与另一台计算机进行通信。在网络编程中,Socket 是一种抽象的表示,它包括了 IP 地址
和端口号
。通过 Socket,程序可以向网络上的另一台计算机发送数据或者接收数据。
如有疏漏,期待指出。
Socket 的类型
Socket 根据协议类型主要分为两类:
-
流式套接字(SOCK_STREAM):使用 TCP(Transmission Control Protocol)协议,提供面向连接的稳定数据传输。
-
数据报套接字(SOCK_DGRAM):使用 UDP(User Datagram Protocol)协议,提供无连接的数据传输。
本文主要讲解基于 TCP 协议的流式套接字。变成部分基于Python
的Socket
库。
首先梳理服务器端和客户端Socket从创建到关闭的过程。
服务器端
服务器端的套接字:创建、绑定、监听、接受、读写、关闭。
形象地描述一下服务器端。服务器端就相当于是一家之主,邀请客人来做客。创建套接字就相当于创建了一个人。绑定IP和端口就是让这个人去到一扇对外的大门前,和这扇大门“绑定”。监听就相当于这个人一直盯着门口,看看有没有客人来。一旦监听到这个门有客人来敲门,就可以选择开门接受连接。接受连接后,便可以进行读写,互相交流。兴致尽矣,便是送客,关闭连接。
创建一个套接字,给它绑定上IP和端口,然后开始监听这个IP和端口。如果IP正确的客户端向这个端口发来连接请求,那么套接字就会接受这个请求,并与客户端开始读写数据。直到双方关闭连接。
-
创建套接字(socket()):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
AF_INET
表示使用 IPv4 地址。SOCK_STREAM
表示使用 TCP 协议。
-
绑定地址(bind()):
server_socket.bind(('IP_ADDRESS', PORT))
- 绑定 IP 地址和端口号,使得服务器可以在指定地址和端口上监听客户端请求。
-
监听连接(listen()):
server_socket.listen(BACKLOG)
- 开始监听端口,
BACKLOG
指定等待连接的最大数量。
- 开始监听端口,
-
接受连接(accept()):
client_socket, client_address = server_socket.accept()
- 阻塞等待客户端连接,一旦有客户端连接请求,就建立连接,并返回一个新的套接字对象
client_socket
以进行通信。
- 阻塞等待客户端连接,一旦有客户端连接请求,就建立连接,并返回一个新的套接字对象
-
读写数据(read()/write()):
data = client_socket.recv(BUFFER_SIZE) client_socket.sendall(response_data)
- 使用
recv()
接收客户端数据,使用sendall()
发送数据给客户端。
- 使用
-
关闭连接(close()):
client_socket.close() server_socket.close()
- 关闭客户端和服务器端的套接字连接。
客户端
客户端的套接字:创建、连接、读写、关闭。
客户端和服务器端的区别在于,它不需要监听和接受。这也很容易理解,因为客户端是客人,服务器端是主人,主人需要时刻注意(监听)着门口有没有人,而客人只需要敲门连接就行了。
创建一个套接字,然后向服务器端发送连接请求。如果服务器端接受请求,那么将开始读写数据,直到关闭。
-
创建套接字(socket()):
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- 同服务器端的创建方式。
-
连接服务器(connect()):
client_socket.connect(('SERVER_IP_ADDRESS', PORT))
- 连接服务器端的 IP 地址和端口号。
connect
是主动发起连接,服务器端的accept
是被动接受连接。
-
读写数据(write()/read()):
client_socket.sendall(request_data) response = client_socket.recv(BUFFER_SIZE)
- 使用
sendall()
发送数据给服务器,使用recv()
接收服务器的数据。
- 使用
-
关闭连接(close()):
client_socket.close()
- 关闭客户端的套接字连接。
完整示例代码
服务器端代码:
import socket
def server_program():
# 创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定IP地址和端口
# 注意,IP地址为空,意味着接受来自所有IP的请求
server_socket.bind(('', 12345))
# 监听连接
server_socket.listen(5)
print("服务器正在监听...")
# 接受连接
client_socket, client_address = server_socket.accept()
print(f"连接来自: {client_address}")
# 接收和发送数据
while True:
data = client_socket.recv(1024)
if not data:
break
print(f"客户端说:{data.decode()}")
client_socket.sendall(data)
# 关闭连接
client_socket.close()
server_socket.close()
if __name__ == '__main__':
server_program()
客户端代码:
import socket
def client_program():
# 创建套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器
client_socket.connect(('127.0.0.1', 65432))
# 发送和接收数据
message = "我们连接上了捏!"
client_socket.sendall(message.encode())
data = client_socket.recv(1024)
print(f"服务器说:{data.decode()}")
# 关闭连接
client_socket.close()
if __name__ == '__main__':
client_program()
以上代码是客户端与服务器的通信,由于是单线程的,所以服务器端同时只能接受一个客户的连接,无法做到接受并发的多客户数据。
所以,接下来引入多线程,使用Python
的threading
库。
Threading
什么是线程?
线程是计算机科学中的一个基本概念,它是比进程更小的执行单元。一个进程可以包含多个线程,每个线程都共享进程的资源,如内存、文件描述符等。线程的引入是为了提高程序的并发性,利用多核处理器的优势。
简单地说,线程就像是一家公司的多个员工,每个员工都可以同时工作,完成不同的任务,而这些员工共享公司提供的资源,比如办公室、电脑和文具。公司(进程)是一个整体,而员工(线程)是这个整体中的个体,他们协同工作以完成更大的目标。
而多线程是指在同一个进程中同时运行多个线程。这些线程独立执行,但共享进程的资源。通过多线程,程序可以在同一时间处理多个任务,从而提高效率和响应速度。
举个例子,假设你在一家快餐店工作,你需要同时处理多个订单。你可以一个一个地处理订单,这样的话,顾客可能会等得不耐烦。或者,你可以雇用多名员工,每个人处理一个订单,这样所有订单可以同时进行处理,顾客的等待时间就会大大减少。(这例子是chatGPT写得)
多线程能做到什么?
多线程主要有以下几个优势:
-
提高程序的并发性和响应速度:
多线程可以使程序在处理多个任务时更加高效。例如,一个 Web 服务器可以使用多线程来同时处理多个客户端的请求,而不会因为一个请求而阻塞其他请求。 -
更好地利用多核处理器的性能:
在现代计算机中,多核处理器非常普遍。多线程可以让程序在多个核心上并行运行,从而充分利用硬件资源,提高程序的执行效率。 -
提高程序的灵活性和可扩展性:
多线程可以让程序更灵活地处理复杂任务。例如,一个复杂的数据处理程序可以将不同的数据处理任务分配给不同的线程,从而更高效地完成任务。
在详细阐明了线程和多线程的概念之后,我们接下来将进入代码部分,讲解如何在 Python 中实现多线程服务器和客户端通信。
编程实践
在本节中,我们将基于之前的单线程示例,使用 Python 的 threading
库实现多线程的服务器和客户端通信。通过引入多线程,服务器能够同时处理多个客户端的连接请求,提高并发性和响应速度。
多线程服务器实现
我们需要修改服务器端代码,使其能够处理多个客户端的连接。我们将为每个客户端连接创建一个新线程,以便并发处理每个客户端的请求。
首先,因为会有很多客人来连接,所以我们的accept
部分需要进行无限循环。每accept
一次,就要创建一个新的线程。
形象地讲(这不是chatGPT写得),主人每接收到一个客人,就创建一个分身(线程),让这个分身去执行接待客人,也就是收发数据。
while True:
# 接受客户端连接
client_socket, client_address = server_socket.accept()
# 创建新线程处理客户端连接
client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
client_thread.start()
线程创建后需要调用.start()
启动。
我们发现这种创建线程的语法接受两个参数:target
和args
。
client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
其中target
是线程(分身)需要去执行的函数,在这里就是收发数据的函数,也就是handle_client
函数。这个函数会负责招待客人(收发数据)。而args
就是target
函数所需要的变量传参。
# 处理客户端连接的函数
def handle_client(client_socket, client_address):
print(f"新连接:{client_address}")
while True:
try:
# 接收数据
data = client_socket.recv(1024)
if not data:
break
print(f"来自 {client_address} 的消息:{data.decode()}")
# 回传数据
client_socket.sendall(data)
except ConnectionResetError:
break
# 关闭客户端连接
client_socket.close()
print(f"连接断开:{client_address}")
多线程服务器代码:
import socket
import threading
# 处理客户端连接的函数
def handle_client(client_socket, client_address):
print(f"新连接:{client_address}")
while True:
try:
# 接收数据
data = client_socket.recv(1024)
if not data:
break
print(f"来自 {client_address} 的消息:{data.decode()}")
# 回传数据
client_socket.sendall(data)
except ConnectionResetError:
break
# 关闭客户端连接
client_socket.close()
print(f"连接断开:{client_address}")
def server_program():
# 创建套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定IP地址和端口
server_socket.bind(('0.0.0.0', 12345))
# 监听连接
server_socket.listen(5)
print("服务器正在监听...")
while True:
# 接受客户端连接
client_socket, client_address = server_socket.accept()
# 创建新线程处理客户端连接
client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
client_thread.start()
if __name__ == '__main__':
server_program()
在这个多线程服务器代码中,我们做了以下修改:
-
创建线程处理客户端连接:
每当有新的客户端连接请求时,我们创建一个新线程,调用handle_client
函数来处理该连接。 -
处理客户端连接的函数
handle_client
:
在这个函数中,我们接收和发送数据。每个客户端连接都在独立的线程中运行,确保同时处理多个客户端请求。
多线程客户端实现
客户端代码不需要太多修改,因为它本身并不需要处理并发请求。不过,为了测试服务器的多线程能力,我们可以启动多个客户端连接。
多线程客户端代码:
import socket
def client_program():
# 创建套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器
client_socket.connect(('127.0.0.1', 12345))
try:
while True:
# 发送消息
message = input("请输入要发送的信息:")
if message.lower() == 'exit':
break
client_socket.sendall(message.encode())
# 接收服务器的回传消息
data = client_socket.recv(1024)
print(f"服务器回应:{data.decode()}")
finally:
# 关闭连接
client_socket.close()
if __name__ == '__main__':
client_program()
测试多线程服务器
要测试多线程服务器,我们可以启动服务器程序,并在多个终端窗口中运行客户端程序。这样可以模拟多个客户端同时连接服务器,并验证服务器能否正确处理并发连接。
-
启动服务器:
在一个终端窗口中运行服务器程序:python server_program.py
-
启动多个客户端:
在其他终端窗口中运行多个客户端程序:python client_program.py
-
测试通信:
在客户端程序中输入消息,并观察服务器端的输出,验证服务器是否正确接收并回应每个客户端的消息。
通过多线程的实现,服务器能够同时处理多个客户端的连接请求,提高了系统的并发能力和响应速度。
引入TEA加密
接下来,我们将引入 TEA 加密算法,对客户端和服务器之间传输的数据进行加解密,以确保通信的安全性。
加密算法部分可自行选择替换,选一种自己认为安全的,或者选几种进行对比。
TEA加密算法
TEA(Tiny Encryption Algorithm)是一种简单高效的对称加密算法。它使用一个128位的密钥,进行多轮数据加密操作。TEA 算法的主要特点是速度快、实现简单,非常适合嵌入式系统和资源受限的环境。
TEA加解密实现:
import struct
# TEA 加密函数
def tea_encrypt(plain_text, key):
delta = 0x9e3779b9
v0, v1 = struct.unpack('>2L', plain_text)
k = struct.unpack('>4L', key)
sum = 0
for _ in range(32):
sum += delta
v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1])
v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3])
return struct.pack('>2L', v0, v1)
# TEA 解密函数
def tea_decrypt(cipher_text, key):
delta = 0x9e3779b9
v0, v1 = struct.unpack('>2L', cipher_text)
k = struct.unpack('>4L', key)
sum = delta * 32
for _ in range(32):
v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3])
v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1])
sum -= delta
return struct.pack('>2L', v0, v1)
通过引入 TEA 加密算法,我们实现了对客户端和服务器之间传输的数据进行加解密处理,确保了通信的安全性。这样,我们不仅能够实现多线程的并发处理,还能够保证通信的机密性和完整性。
最终成品
上述代码还有点缺点:
- 客户端发送的信息只有服务器能看见,其它客户看不见。
- 每个客户端和服务器只能一问一答,极其混乱。
- 无法指定服务器IP。
- 没有昵称,极其无聊。
- TEA算法代码和服务器/客户端混到一起了。
解决方案:
- 服务器增加广播机制,把信息广播给所有人。
- 客户端同时启动收发线程。
- 单独设置变量,指定服务器IPv4。
- 加入昵称,并广播昵称。
- 单独构建
my_crypt.py
并在服务器/客户端中import
,更方便替换加解密算法。
使用方法:
把server.py
和my_crypt.py
放在服务端同一目录下;把user.py
和my_crypt.py
放在客户端同一目录下,即可运行。
最终代码
server.py
import socket
import threading
from my_crypt import encrypt, decrypt
# 设置TEA加密密钥
key = b'1234567890abcdef'
# 客户端线程处理函数
def client_thread(conn, addr, clients, nicknames):
# 接收客户端发送的昵称
encrypted_nickname = conn.recv(1024)
nickname = decrypt(encrypted_nickname, key).decode('utf-8')
# 存储用户和用户名
clients.append(conn)
nicknames[conn] = nickname
# 广播用户加入消息
welcome_message = f"{nickname} 加入了聊天室!"
conn.send(encrypt(welcome_message.encode('utf-8'), key))
broadcast(welcome_message, conn, clients, nicknames)
while True:
try:
encrypted_message = conn.recv(1024)
message = decrypt(encrypted_message, key).decode('utf-8')
if message:
# 广播用户消息
formatted_message = f"{nickname} 说: {message}"
print(formatted_message)
broadcast(formatted_message, conn, clients, nicknames)
else:
# 处理断开连接
remove(conn, clients, nicknames)
break
except:
continue
# 广播消息到所有客户端
def broadcast(message, connection, clients, nicknames):
encrypted_message = encrypt(message.encode('utf-8'), key)
for client in clients:
if client != connection:
try:
client.send(encrypted_message)
except:
remove(client, clients, nicknames)
# 从列表中移除客户端
def remove(connection, clients, nicknames):
if connection in clients:
leave_message = f"{nicknames[connection]} 离开了聊天室。"
broadcast(leave_message, connection, clients, nicknames) # 广播用户离开消息
clients.remove(connection)
del nicknames[connection]
connection.close()
# 服务器主程序
def server_program():
host = ''
port = 12345
clients = []
nicknames = {}
# 创建,绑定,监听
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen(100)
while True:
# 接受新的连接并启动线程
conn, addr = server_socket.accept()
threading.Thread(target=client_thread, args=(conn, addr, clients, nicknames)).start()
if __name__ == '__main__':
server_program()
user.py
import socket
import threading
from my_crypt import encrypt, decrypt
# 设置TEA加密密钥
key = b'1234567890abcdef'
# 发送消息线程
def send_msg(client_socket, nickname):
while True:
message = input()
encrypted_message = encrypt(message.encode('utf-8'), key)
client_socket.send(encrypted_message)
if message.lower().strip() == 'bye':
client_socket.close()
break
# 接收消息线程
def receive_msg(client_socket):
while True:
try:
encrypted_data = client_socket.recv(1024)
if not encrypted_data:
break
data = decrypt(encrypted_data, key).decode('utf-8')
print(data) # 直接打印接收到的消息
except:
print("已断开连接")
break
# 客户端主程序
def client_program():
host = input("请输入服务器的IPv4地址: ")
port = 12345
nickname = input("请输入您的昵称:")
# 创建,连接
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((host, port))
print("连接成功!")
# 发送昵称到服务器
encrypted_nickname = encrypt(nickname.encode('utf-8'), key)
client_socket.send(encrypted_nickname)
# 创建收发线程
threading.Thread(target=send_msg, args=(client_socket, nickname)).start()
threading.Thread(target=receive_msg, args=(client_socket,)).start()
if __name__ == '__main__':
client_program()
my_crypt.py
import struct
# PKCS7 填充函数
def pad(data):
padding_length = 8 - len(data) % 8
return data + bytes([padding_length] * padding_length)
# PKCS7 去填充函数
def unpad(data):
padding_length = data[-1]
return data[:-padding_length]
# TEA 加密函数
def encrypt(plain_text, key):
plain_text = pad(plain_text)
cipher_text = b''
delta = 0x9e3779b9
k = struct.unpack('>4L', key)
for i in range(0, len(plain_text), 8):
v0, v1 = struct.unpack('>2L', plain_text[i:i+8])
sum = 0
for _ in range(32):
sum = (sum + delta) & 0xffffffff
v0 = (v0 + ((v1 << 4) + k[0] ^ v1 + sum ^ (v1 >> 5) + k[1])) & 0xffffffff
v1 = (v1 + ((v0 << 4) + k[2] ^ v0 + sum ^ (v0 >> 5) + k[3])) & 0xffffffff
cipher_text += struct.pack('>2L', v0, v1)
return cipher_text
# TEA 解密函数
def decrypt(cipher_text, key):
plain_text = b''
delta = 0x9e3779b9
k = struct.unpack('>4L', key)
for i in range(0, len(cipher_text), 8):
v0, v1 = struct.unpack('>2L', cipher_text[i:i+8])
sum = (delta * 32) & 0xffffffff
for _ in range(32):
v1 = (v1 - ((v0 << 4) + k[2] ^ v0 + sum ^ (v0 >> 5) + k[3])) & 0xffffffff
v0 = (v0 - ((v1 << 4) + k[0] ^ v1 + sum ^ (v1 >> 5) + k[1])) & 0xffffffff
sum = (sum - delta) & 0xffffffff
plain_text += struct.pack('>2L', v0, v1)
return unpad(plain_text)