实验所用语言为Python,参考前辈们的比较多,在此仅作记录
参考/学习:
Python Socket编程
Python threading编程
参考1
参考2
通过本实验,学习采用Socket(套接字)设计简单的网络数据收发程序,理解应用数据包是如何通过传输层进行传送的。
Socket(套接字)是一种抽象层,应用程序通过它来发送和接收数据,就像应用程序打开一个文件句柄,将数据读写到稳定的存储器上一样。一个socket允许应用程序添加到网络中,并与处于同一个网络中的其他应用程序进行通信。一台计算机上的应用程序向socket写入的信息能够被另一台计算机上的另一个应用程序读取,反之亦然。
不同类型的socket与不同类型的底层协议族以及同一协议族中的不同协议栈相关联。现在TCP/IP协议族中的主要socket类型为流套接字(sockets sockets)和数据报套接字(datagram sockets)。流套接字将TCP作为其端对端协议(底层使用IP协议),提供了一个可信赖的字节流服务。一个TCP/IP流套接字代表了TCP连接的一端。数据报套接字使用UDP协议(底层同样使用IP协议),提供了一个"尽力而为"(best-effort)的数据报服务,应用程序可以通过它发送最长65500字节的个人信息。一个TCP/IP套接字由一个互联网地址,一个端对端协议(TCP或UDP协议)以及一个端口号唯一确定。
# === TCP 服务端程序 ===
# 导入socket 库
from socket import *
# 主机地址为0.0.0.0, 表示绑定本机所有网络接口ip地址
# 主机地址127.0.0.1表示主机环回地址
# 等待客户端来连接
IP = '127.0.0.1'
# 端口号
PORT = 50000
# 定义一次从socket缓冲区最多读入512个字节数据
BUFLEN = 512
# 实例化一个socket对象
# 参数 AF_INET表示该socket网络层使用IP协议
# 参数SOCK_STREAM 表示socket传输层使用tcp协议
listenSocket = socket(AF_INET, SOCK_STREAM)
# socket绑定地址和端口
listenSocket.bind((IP, PORT))
# 使用socket处于监听状态, 等待客户端的连接请求
# 参数5表示,最多接收多少个等待连接的客户端
listenSocket.listen(5)
print(f"服务端启动成功, 在{PORT}端口等待客户端连接...")
# 当没有客户端连接时,服务端程序将会在此处阻塞,等待客户端连接
dataSocket, addr = listenSocket.accept()
# TCP连接建立之后accept()方法将会返回一个新的socket(用来收发数据)和一个客户端地址
print('接收一个客户端连接:', addr)
while True:
# 尝试读取对方发送的消息
# BUFLEN 指定从接收缓冲里最多读取多少字节
recved = dataSocket.recv(BUFLEN)
# 如果返回空bytes, 表示对方关闭了连接
# 退出循环,结束消息收发
if not recved:
break
# 读取的字节数据是bytes类型, 需要解码为字符串
info = recved.decode()
print(f'收到对方信息: {info}')
# 发送的数据类型必须是bytes, 所以要编码
dataSocket.send(f'服务端接收到了信息 {info}'.encode())
# 服务端也调用close()关闭socket
dataSocket.close()
listenSocket.close()
# === TCP客户端程序 ===
from socket import *
IP = '127.0.0.1'
SERVER_PORT = 60000
BUFLEN = 512
# 实例化一个socket对象,指明协议
dataSocket = socket(AF_INET, SOCK_STREAM)
# 连接服务端socket
dataSocket.connect((IP, SERVER_PORT))
while True:
# 从终端读入用户输入的字符串
toSend = input('>>')
if toSend == 'exit':
break
# 发送消息,也要编码为bytes
dataSocket.send(toSend.encode())
# 等待接收服务端的消息
recved = dataSocket.recv(BUFLEN)
# 如果返回空bytes, 表示对方关闭了连接
if not recved:
break
# 打印读取的信息
print(recved.decode())
dataSocket.close()
设置服务端在收到客户端消息之后将收到的消息再发送回去,验证二者收发能力正常。
# === UDP服务端程序 ===
# 导入socket 库
from socket import *
# 主机地址为0.0.0.0, 表示绑定本机所有网络接口ip地址
# 主机地址127.0.0.1表示主机环回地址
# 等待客户端来连接
IP = '127.0.0.1'
# 端口号
PORT = 50000
# 定义一次从socket缓冲区最多读入512个字节数据
BUFLEN = 512
# 实例化一个socket对象
# 参数 AF_INET表示该socket网络层使用IP协议
# 参数SOCK_DGRAM 表示socket传输层使用udp协议
listenSocket = socket(AF_INET, SOCK_DGRAM)
# socket绑定地址和端口
listenSocket.bind((IP, PORT))
print(f"服务端启动成功, 在{PORT}端口等待客户端连接...")
while True:
# 接收UDP套接字的数据,返回接收到的数据和套接字地址
recved, addr = listenSocket.recvfrom(BUFLEN)
# 如果返回空bytes, 表示对方关闭了连接
# 退出循环,结束消息收发
if not recved:
break
# 读取的字节数据是bytes类型, 需要解码为字符串
info = recved.decode()
print(f'收到对方信息: {info}')
# 发送的数据类型必须是bytes, 所以要编码
listenSocket.sendto(f'服务端接收到了信息 {info}'.encode(), addr)
# 服务端也调用close()关闭socket
listenSocket.close()
# === UDP客户端套接字 ===
from socket import *
IP = '127.0.0.1'
SERVER_PORT = 50000
BUFLEN = 512
# 实例化一个socket对象,指明UDP协议
dataSocket = socket(AF_INET, SOCK_DGRAM)
while True:
# 从终端读入用户输入的字符串
toSend = input('>>')
if toSend == 'exit':
break
# 发送UDP数据, 将数据发送到套接字
dataSocket.sendto(toSend.encode(), (IP, SERVER_PORT))
# 接收服务端的消息
recved, addr = dataSocket.recvfrom(BUFLEN)
# 如果返回空bytes, 表示对方关闭了连接
if not recved:
break
# 打印读取的信息
print(recved.decode())
dataSocket.close()
设置服务端在收到客户端消息之后将收到的消息再发送回去,验证二者收发能力正常。
当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然其在连接建立后即可向服务器端发送数据,服务器端在处理完已有客户端的请求前,却不会对新的客户端作出响应。
并行服务器:可以单独处理没一个连接,且不会产生干扰。并行服务器分为两种:一客户一线程和线程池。
每个新线程都会消耗系统资源:创建一个线程将占用CPU周期,而且每个线程都自己的数据结构(如,栈)也要消耗系统内存。另外,当一个线程阻塞(block)时,JVM将保存其状态,选择另外一个线程运行,并在上下文转换(context switch)时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的系统资源。这将最终导致系统花费更多的时间来处理上下文转换和线程管理,更少的时间来对连接进行服务。那种情况下,加入一个额外的线程实际上可能增加客户端总服务时间。
我们可以通过限制总线程数并重复使用线程来避免这个问题。与为每个连接创建一个新的线程不同,服务器在启动时创建一个由固定数量线程组成的线程池(thread pool)。当一个新的客户端连接请求传入服务器,它将交给线程池中的一个线程处理。当该线程处理完这个客户端后,又返回线程池,并为下一次请求处理做好准备。如果连接请求到达服务器时,线程池中的所有线程都已经被占用,它们则在一个队列中等待,直到有空闲的线程可用。
# === 多线程 TCP 服务端 ===
# 导入库
from socket import *
import threading
# 主机地址为0.0.0.0, 表示绑定本机所有网络接口ip地址
# 主机地址127.0.0.1表示主机环回地址
# 等待客户端来连接
IP = '127.0.0.1'
# 端口号
PORT = 50000
# 定义一次从socket缓冲区最多读入512个字节数据
BUFLEN = 512
# 实例化一个socket对象
# 参数 AF_INET表示该socket网络层使用IP协议
# 参数SOCK_STREAM 表示socket传输层使用tcp协议
listenSocket = socket(AF_INET, SOCK_STREAM)
# socket绑定地址和端口
listenSocket.bind((IP, PORT))
# 使用socket处于监听状态, 等待客户端的连接请求
# 参数5表示,最多接收多少个等待连接的客户端
listenSocket.listen(5)
print(f"服务端启动成功, 在{PORT}端口等待客户端连接...")
# 接收到连接之后进行通信的方法
def TCP_Link(dataSocket, addr):
while True:
# 尝试读取对方发送的消息
# BUFLEN 指定从接收缓冲里最多读取多少字节
recved = dataSocket.recv(BUFLEN)
# 如果返回空bytes, 表示对方关闭了连接
# 退出循环,结束消息收发
if not recved:
break
# 读取的字节数据是bytes类型, 需要解码为字符串
info = recved.decode()
print(f'收到来自{addr}的信息: {info}')
# 发送的数据类型必须是bytes, 所以要编码
dataSocket.send(f'服务端接收到了信息 {info}'.encode())
# 服务端也调用close()关闭socket
print(f'客户端{addr}断开连接')
dataSocket.close()
# 服务端socket 持续接收客户端连接
while True:
# 当没有客户端连接时,服务端程序将会在此处阻塞,等待客户端连接
dataSocket, addr = listenSocket.accept()
# TCP连接建立之后accept()方法将会返回一个新的socket(用来收发数据)和一个客户端地址
print('接收一个客户端连接:', addr)
# 接收到一个连接之后, 为该连接建立新的线程
addThread = threading.Thread(target=TCP_Link, args=(dataSocket, addr))
# 运行该线程
addThread.start()
[多线程的客户端与使用TCP的客户端相同,可以复制多个程序与服务端连接模拟多线程的情况]
这里以服务端同时接收两个客户端连接为例,验证服务端具备多线程处理客户端发送信息的能力。
# === 线程池 TCP 服务端 ===
# 导入库
from socket import *
from concurrent.futures import ThreadPoolExecutor
# 主机地址为0.0.0.0, 表示绑定本机所有网络接口ip地址
# 主机地址127.0.0.1表示主机环回地址
# 等待客户端来连接
IP = '127.0.0.1'
# 端口号
PORT = 60000
# 定义一次从socket缓冲区最多读入512个字节数据
BUFLEN = 512
# 实例化一个socket对象
# 参数 AF_INET表示该socket网络层使用IP协议
# 参数SOCK_STREAM 表示socket传输层使用tcp协议
listenSocket = socket(AF_INET, SOCK_STREAM)
# socket绑定地址和端口
listenSocket.bind((IP, PORT))
# 使用socket处于监听状态, 等待客户端的连接请求
# 参数5表示,最多接收多少个等待连接的客户端
listenSocket.listen(5)
print(f"服务端启动成功, 在{PORT}端口等待客户端连接...")
# 接收到连接之后进行通信的方法
def TCP_Link(dataSocket, addr):
while True:
# 尝试读取对方发送的消息
# BUFLEN 指定从接收缓冲里最多读取多少字节
recved = dataSocket.recv(BUFLEN)
# 如果返回空bytes, 表示对方关闭了连接
# 退出循环,结束消息收发
if not recved:
break
# 读取的字节数据是bytes类型, 需要解码为字符串
info = recved.decode()
print(f'收到来自{addr}的信息: {info}')
# 发送的数据类型必须是bytes, 所以要编码
dataSocket.send(f'服务端接收到了信息 {info}'.encode())
# 服务端也调用close()关闭socket
print(f'客户端{addr}断开连接')
dataSocket.close()
# 建立一个线程池
thread_pool = ThreadPoolExecutor(max_workers=5)
# 服务端socket 持续接收连接
while True:
# 当没有客户端连接时,服务端程序将会在此处阻塞,等待客户端连接
dataSocket, addr = listenSocket.accept()
# TCP连接建立之后accept()方法将会返回一个新的socket(用来收发数据)和一个客户端地址
print('接收一个客户端连接:', addr)
# 接收到一个连接之后, 为该连接建立新的线程
thread_pool.submit(TCP_Link, dataSocket, addr)
[线程池的客户端与使用TCP的客户端相同,可以复制多个程序与服务端连接模拟多线程的情况]
这里以服务端同时接收两个客户端连接为例,验证服务端具备多线程处理客户端发送信息的能力。
# === Chat程序编写 TCP 服务端 ===
# 导入库
from socket import *
import threading
import os
import json
# 主机地址为0.0.0.0, 表示绑定本机所有网络接口ip地址
# 主机地址127.0.0.1表示主机环回地址
# 等待客户端来连接
IP = '127.0.0.1'
# 端口号
PORT = 40000
# 定义一次从socket缓冲区最多读入512个字节数据
BUFLEN = 512
# 上传文件的源地址
UPLOAD_PATH = r"ServerUPLOAD/计算机网络-自顶向下方法第七版.pdf"
# 文件下载地址
DOWNLOAD_PATH = r"ServerDOWNLOAD"
# 实例化一个socket对象
# 参数 AF_INET表示该socket网络层使用IP协议
# 参数SOCK_STREAM 表示socket传输层使用tcp协议
listenSocket = socket(AF_INET, SOCK_STREAM)
# socket绑定地址和端口
listenSocket.bind((IP, PORT))
# 使用socket处于监听状态, 等待客户端的连接请求
# 参数5表示,最多接收多少个等待连接的客户端
listenSocket.listen(5)
print(f"服务端启动成功, 在{PORT}端口等待客户端连接...")
# 定义服务端连接方法
def Chat(dataSocket, addr):
while True:
# 尝试读取对方发送的消息
# BUFLEN 指定从接收缓冲里最多读取多少字节
recved = dataSocket.recv(BUFLEN).decode()
# 如果返回空bytes, 表示对方关闭了连接
# 退出循环,结束消息收发
if not recved:
break
# 读取的字节数据是bytes类型, 需要解码为字符串
# 客户端聊天
if recved == "chat":
print(f'收到来自{addr}的聊天请求')
recved = dataSocket.recv(BUFLEN).decode()
print(f'收到来自{addr}的信息: {recved}')
toSend = input(f"发送给{addr}>>")
dataSocket.send(toSend.encode())
# 客户端向服务端请求文件
elif recved == "get":
print(f'收到来自{addr}的文件请求')
# 获取将要上传文件的名称、大小
file_name = os.path.basename(UPLOAD_PATH)
file_size = os.path.getsize(UPLOAD_PATH)
dict = {
'filename': file_name,
'filesize': file_size
}
# 将文件基本信息编码成JSON发送给客户端
fileinfo = json.dumps(dict)
dataSocket.send(fileinfo.encode('utf-8'))
with open(UPLOAD_PATH, 'rb') as f:
data = f.read()
dataSocket.sendall(data)
print(f'已成功将文件{file_name}上传给{addr}')
# 收到来自用户的文件
elif recved == "deliver":
print(f'收到来自{addr}的上传请求')
fileinfo = dataSocket.recv(BUFLEN)
# 将从客户端收到的JSON文件信息解码
dict = json.loads(fileinfo)
file_name = dict['filename']
file_size = dict['filesize']
print(f'收到来自{addr}的文件:{file_name}, 文件大小:{file_size}')
recv_size = 0
recv_file = b''
f = open(DOWNLOAD_PATH + '\\' + file_name, 'wb')
while recv_size < file_size:
if file_size - recv_size > BUFLEN:
recv_file = dataSocket.recv(BUFLEN)
f.write(recv_file)
recv_size += len(recv_file)
else:
recv_file = dataSocket.recv(file_size - recv_size)
recv_size += len(recv_file)
f.write(recv_file)
f.close()
print('文件接收成功!')
elif recved == "exit":
print(f'客户端{addr}断开连接')
dataSocket.close()
break
# 服务端socket 持续接收客户端连接
while True:
# 当没有客户端连接时, 服务端程序在此处阻塞, 等待客户端连接
dataSocket, addr = listenSocket.accept()
# TCP连接建立之后accept()方法将会返回一个新的socket(用来收发数据)和一个客户端地址
print('接收一个客户端连接:', addr)
# 接收到一个连接之后, 为该连接建立新的线程
addThread = threading.Thread(target=Chat, args=(dataSocket, addr))
# 运行该线程
addThread.start()
# === Chat程序编写 TCP 客户端 ===
# 导入库
from socket import *
import os
import json
IP = '127.0.0.1'
SERVER_PORT = 40000
BUFLEN = 512
UPLOAD_PATH = r"ClientUPLOAD/编译原理(第2版).pdf"
DOWNLOAD_PATH = r"ClientDOWNLOAD"
# 实例化一个socket对象,指明协议
dataSocket = socket(AF_INET, SOCK_STREAM)
# 连接服务端socket
dataSocket.connect((IP, SERVER_PORT))
print("已成功与服务端取得连接, 键入chat、get、deliver进行不同功能")
while True:
# 从终端读入用户输入的字符串
toSend = input('>>')
if toSend == 'exit':
break
# 聊天
if toSend == 'chat':
dataSocket.send(toSend.encode())
toSend = input('输入消息内容:')
dataSocket.send(toSend.encode())
print('消息已发送, 等待服务端回应')
# 等待接收服务端的消息
recved = dataSocket.recv(BUFLEN)
# 如果返回空bytes, 表示对方关闭了连接
if not recved:
break
# 打印读取的信息
print('收到来自服务端回应:' + recved.decode())
# 向服务端请求文件
elif toSend == 'get':
dataSocket.send(toSend.encode())
fileinfo = dataSocket.recv(BUFLEN)
# 将从客户端收到的JSON文件信息解码
dict = json.loads(fileinfo)
file_name = dict['filename']
file_size = dict['filesize']
print(f'收到来自服务端的文件:{file_name}, 文件大小:{file_size}')
recv_size = 0
recv_file = b''
f = open(DOWNLOAD_PATH + '\\' + file_name, 'wb')
while recv_size < file_size:
if file_size - recv_size > BUFLEN:
recv_file = dataSocket.recv(BUFLEN)
f.write(recv_file)
recv_size += len(recv_file)
else:
recv_file = dataSocket.recv(file_size - recv_size)
recv_size += len(recv_file)
f.write(recv_file)
f.close()
print('文件接收成功!')
elif toSend == 'deliver':
dataSocket.send(toSend.encode())
# 获取将要上传文件的名称、大小
file_name = os.path.basename(UPLOAD_PATH)
file_size = os.path.getsize(UPLOAD_PATH)
dict = {
'filename': file_name,
'filesize': file_size
}
# 将文件基本信息编码成JSON发送给客户端
fileinfo = json.dumps(dict)
dataSocket.send(fileinfo.encode('utf-8'))
with open(UPLOAD_PATH, 'rb') as f:
data = f.read()
dataSocket.sendall(data)
print(f'已成功将文件{file_name}上传给服务端')
dataSocket.close()
互相发送消息
在聊天模式下,目前的效果时二者必须交替发送消息,并且客户端在每次发消息时需要先输入chat指令,比较麻烦,后续有思路了再做修改。
客户端向服务端发文件
这里为方便演示,将发送文件的路径固定,客户端要上传的文件固定为ClientUPLOAD下的文件,服务端在接收之后固定存放在ServerDOWNLOAD目录下。
客户端键入deliver表示向服务端发送文件。
之后服务端ServerDOWNLOAD目录下已成功接收到文件(pycharm文件显示较慢,可以在电脑文件夹查看,传输速度比复制粘贴更快)。
服务端向客户端发文件
客户端键入get请求表示向服务端请求文件,服务端将预设路径下的文件发送给客户端。
可以看到客户端成功接收到来自服务端发送的文件。
通过本次实验,对socket编程有了初步的了解,学习了如何使用套接字采用TCP进行数据的收发、用UDP进行数据的收发,socket是应用层与TCP/IP协议中间的抽象层,作为一组接口,其将复杂的TCP/IP协议隐藏在socket接口后面,减轻了程序员编程的负担;同时也对比了多线程、线程池两种不同的多线程通信方案的实现方式和差别,相比于多线程方式,线程池预先创建多个线程,在需要新线程的时候将提前创建的线程直接取出来使用,在效率上是比多线程实现方式更优的,但两种方式都有自己的应用场景;最后利用python语言尝试编写了互传文件的程序,即使是简单的程序,在细节上依旧有很多地方需要注意,总之,这次也是收获满满呀。