Skip to content

TCP网络聊天

在此之前...

首先,我们要了解TCP是什么? TCP(传输控制协议,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它在互联网协议族(Internet Protocol Suite)中属于传输层,与IP协议(Internet Protocol)共同构成了网络通信的基础,通常被合称为TCP/IP协议。 以下是TCP协议的主要特点:

  1. 面向连接:在数据传输之前,TCP需要通过“三次握手”过程建立连接,确保数据传输的双方都准备好进行通信。
  2. 可靠性:TCP确保传输的数据不重复、不丢失、顺序正确。它通过序列号、确认应答、重传机制、流量控制、拥塞控制等技术来保证数据的可靠传输。
  3. 基于字节流:TCP中的“流”指的是流入或流出的字节序列。发送端将数据分割成适当大小的段,而接收端则按照顺序将这些段重新组装成完整的数据。
  4. 端到端通信:TCP直接作用于两端的端系统(例如,两台计算机),而不关心数据在网络中的具体传输路径。
  5. 全双工通信:TCP允许通信双方的数据同时双向传输。
  6. 流量控制:TCP使用窗口大小来控制发送数据的速率,以避免接收方处理不过来。
  7. 拥塞控制:TCP通过减少其发送数据的速率来响应它所认为的网络拥塞情况,常见的算法有慢启动、拥塞避免、快速重传和快速恢复。

TCP协议广泛应用于各种需要高可靠性的应用场景,如网页浏览(HTTP)、文件传输(FTP)、电子邮件(SMTP)等。由于它的可靠性,TCP比UDP(用户数据报协议,User Datagram Protocol)等其他传输层协议在数据传输上更为可靠,但通常也会有更高的传输延迟。
我们可以通过TCP网络传输协议来搭建一个简单的网络聊天工具。

如何做?

要知道我们需要使用什么库:
需要用到的库非常简单和少量:

  1. socket: socket库是一种编程接口,它允许程序员在遵循TCP协议的基础上,创建网络应用程序以实现网络通信。在TCP协议中提到的核心特性,如连接导向、数据可靠性、流量控制和拥塞控制,都是通过socket库提供的API来实现的。
  2. threading: threading库是Python标准库中的一个模块,它允许程序员创建和管理线程。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。使用threading库可以在Python程序中实现并发执行,这对于执行多个任务或处理I/O密集型操作特别有用。

仅此而已。

逻辑关系

首先,我们要搞清楚编写代码的步骤
选择是客户端还是服务端
选择相应的端可以使用相应的功能服务

服务端

  1. 在输入的ip与端口上创建一个TCP通信服务
  2. 接收每个客户端发来的连接请求
  3. 广播用户的加入和离去
  4. 在聊天时可以让服务器也加入聊天室发送一些人工编写的提醒消息并给服务器命名为"server" 或 "Console"
  5. 在控制时执行各种命令

客户端

  1. 连接到用户输入的服务器中
  2. 输入自己的昵称作为在服务器中的名称
  3. 持续接收消息

分段代码

服务端 server.py

python
import socket
import threading

# 初始化全局变量
clients = []
nicknames = []
server = None


# 发送消息给所有连接的客户端
def broadcast(message):
	for client in clients:
		try:
			client.send(message)
		except Exception as e:
			print(f"无法发送消息至客户端: {e}")
			clients.remove(client)
			client.close()


# 处理每个客户端的线程
def handle_client(client):
	while True:
		try:
			# 接收消息
			message = client.recv(1024)
			if message:
				message_decoded = message.decode('utf-8')
				print(f"{message_decoded}")  # Debug print
				broadcast(message)
			else:
				# No data means disconnected
				break
		except Exception as e:
			print(f"An error occurred: {e}")
			break

	# 客户端断开连接
	index = clients.index(client)
	clients.remove(client)
	client.close()
	nickname = nicknames[index]
	broadcast(f'{nickname} 离开了聊天室'.encode('utf-8'))
	nicknames.remove(nickname)


# 接受客户端连接的函数
def receive_connections(HOST, PORT):
	global server
	server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	server.bind((HOST, PORT))
	server.listen()

	print(f"服务器正运行在: {HOST}:{PORT}")

	while True:
		client, address = server.accept()
		print(f"来自 {str(address)} 的连接")

		# 请求并存储昵称
		client.send('NICK'.encode('utf-8'))
		nickname = client.recv(1024).decode('utf-8')
		nicknames.append(nickname)
		clients.append(client)

		# 广播新客户端的加入
		print(f"{nickname} 进入了聊天室")
		broadcast(f"{nickname} 进入了聊天室".encode('utf-8'))
		client.send('连接成功!'.encode('utf-8'))

		# 开启新线程处理客户端
		thread = threading.Thread(target=handle_client, args=(client,))
		thread.start()


# 服务器管理员发送消息的函数
def admin_message_sender():
	while True:
		message = input()
		if message.lower() == 'quit':
			# 关闭所有客户端连接
			for client in clients:
				client.close()
			# 关闭服务器
			global server
			server.close()
			print("关闭服务器中...")
			break
		broadcast(f"Server: {message}".encode('utf-8'))

def start_server(HOST, PORT):
	# 启动服务器监听线程
	server_thread = threading.Thread(target=receive_connections, args=(HOST, PORT))
	server_thread.start()

	# 启动服务器管理员消息发送线程
	admin_thread = threading.Thread(target=admin_message_sender)
	admin_thread.start()

	# 等待线程结束
	server_thread.join()
	admin_thread.join()

客户端 client.py

python
import socket
import threading

def login_chat_server(HOST,PORT):
    # 服务器的IP地址和端口
    # HOST = '127.0.0.1'
    # PORT = 65432

    # 创建socket对象
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 连接到服务器
    client.connect((HOST, PORT))

    # 输入昵称并发送给服务器
    nickname = input("请输入你的名字: ")
    client.send(nickname.encode('utf-8'))

    # 接收欢迎消息
    message = client.recv(1024)
    print(message.decode('utf-8'))

    # 开启新线程用于持续接收消息
    def receive():
        while True:
            try:
                # 接收消息
                message = client.recv(1024)
                print(message.decode('utf-8'))
            except:
                # 出现连接问题
                print("连接失败!检查网络问题或请联系房主解决")
                client.close()
                break

    # 开启线程
    thread = threading.Thread(target=receive)
    thread.start()

    # 发送消息
    while True:
        message = f'{nickname}: {input("")}'
        client.send(message.encode('utf-8'))

主程序 main.py

python
import server
import client
while True:
	mode = input('开启服务器/加入服务器?>>')
	if mode == '开启服务器' or mode == '开启':
		server.start_server(input('输入ip>>'),int(input('输入端口号>>')))
		break
	if mode == '加入服务器' or mode == '加入':
		client.login_chat_server(input('输入ip>>'),int(input('输入端口号>>')))
		break
	else:
		print("输入错误!请重新输入")
		continue

解释

在分段代码中将server.py与client.py当作了模块在主程序main.py中运行,所以我们可以看到主程序main.py代码十分的简洁短小
代码过长的时候为避免在修补BUG检查的时候眼花缭乱会把一段代码拆分成几段程序以import导入的方式连接在一起
不信的可以看下面的连接代码

连接代码(复杂)

python
import socket
import threading

# 初始化全局变量
clients = []
nicknames = []
server = None


# 发送消息给所有连接的客户端
def broadcast(message):
	for client in clients:
		try:
			client.send(message)
		except Exception as e:
			print(f"无法发送消息至客户端: {e}")
			clients.remove(client)
			client.close()


# 处理每个客户端的线程
def handle_client(client):
	while True:
		try:
			# 接收消息
			message = client.recv(1024)
			if message:
				message_decoded = message.decode('utf-8')
				print(f"{message_decoded}")  # Debug print
				broadcast(message)
			else:
				# No data means disconnected
				break
		except Exception as e:
			print(f"An error occurred: {e}")
			break

	# 客户端断开连接
	index = clients.index(client)
	clients.remove(client)
	client.close()
	nickname = nicknames[index]
	broadcast(f'{nickname} 离开了聊天室'.encode('utf-8'))
	nicknames.remove(nickname)


# 接受客户端连接的函数
def receive_connections(HOST, PORT):
	global server
	server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	server.bind((HOST, PORT))
	server.listen()

	print(f"服务器正运行在: {HOST}:{PORT}")

	while True:
		client, address = server.accept()
		print(f"来自 {str(address)} 的连接")

		# 请求并存储昵称
		client.send('NICK'.encode('utf-8'))
		nickname = client.recv(1024).decode('utf-8')
		nicknames.append(nickname)
		clients.append(client)

		# 广播新客户端的加入
		print(f"{nickname} 进入了聊天室")
		broadcast(f"{nickname} 进入了聊天室".encode('utf-8'))
		client.send('连接成功!'.encode('utf-8'))

		# 开启新线程处理客户端
		thread = threading.Thread(target=handle_client, args=(client,))
		thread.start()


# 服务器管理员发送消息的函数
def admin_message_sender():
	while True:
		message = input()
		if message.lower() == 'quit':
			# 关闭所有客户端连接
			for client in clients:
				client.close()
			# 关闭服务器
			global server
			server.close()
			print("关闭服务器中...")
			break
		broadcast(f"Server: {message}".encode('utf-8'))

def start_server(HOST, PORT):
	# 启动服务器监听线程
	server_thread = threading.Thread(target=receive_connections, args=(HOST, PORT))
	server_thread.start()

	# 启动服务器管理员消息发送线程
	admin_thread = threading.Thread(target=admin_message_sender)
	admin_thread.start()

	# 等待线程结束
	server_thread.join()
	admin_thread.join()

import socket
import threading

def login_chat_server(HOST,PORT):
	# 服务器的IP地址和端口
	# HOST = '127.0.0.1'
	# PORT = 65432

	# 创建socket对象
	client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

	# 连接到服务器
	client.connect((HOST, PORT))

	# 输入昵称并发送给服务器
	nickname = input("请输入你的名字: ")
	client.send(nickname.encode('utf-8'))

	# 接收欢迎消息
	message = client.recv(1024)
	print(message.decode('utf-8'))

	# 开启新线程用于持续接收消息
	def receive():
		while True:
			try:
				# 接收消息
				message = client.recv(1024)
				print(message.decode('utf-8'))
			except:
				# 出现连接问题
				print("连接失败!检查网络问题或请联系房主解决")
				client.close()
				break

	# 开启线程
	thread = threading.Thread(target=receive)
	thread.start()

	# 发送消息
	while True:
		message = f'{nickname}: {input("")}'
		client.send(message.encode('utf-8'))

while True:
	mode = input('开启服务器/加入服务器?>>')
	if mode == '开启服务器' or mode == '开启':
		start_server(input('输入ip>>'),int(input('输入端口号>>')))
		break
	if mode == '加入服务器' or mode == '加入':
		login_chat_server(input('输入ip>>'),int(input('输入端口号>>')))
		break
	else:
		print("输入错误!请重新输入")
		continue

Last word...

见识到了连接代码的恐怖后相信大家都会选择分段代码
但是要注意的是分段代码是由不同的文件组成
并且文件要在同目录下,否则程序会检测不到模块
另外,服务器ip并不是瞎填的
在CMD中写ipconfig查看电脑的ip地址,如果程序无法在该ip上创建服务器的话就会报错
在开启服务器后还需要公网才能与远在他乡的好友聊天,没有公网只能与家里共用一个网络的人使用
在这段代码中可以不限人数的聊天