tcp服务器怎么给客户端发信息?直接说:服务器先和客户端建立一个TCP连接,然后调用send()函数把数据写进socket缓冲区,内核会负责把数据送到客户端。 很多新手以为“发信息”是服务器单方面的事,其实关键在于连接是否还活着,以及你有没有留着那个代表客户端的socket句柄。
tcp服务器怎么给客户端发信息?先理解socket的通信链路
TCP通信和打电话很像,服务器在某个IP和端口上“守着”,客户端拨号过来,双方完成三次握手后,一条虚拟的“通话线路”就建立了,这条线路的两端,各自握着一个socket对象服务器端的socket不代表服务器自己,而是代表这条连接的另一头,也就是那个客户端。
行业内有个共识:TCP连接是点对点的,不存在局域网里那种到处乱喊的广播,所以服务器想给客户端发信息,必须先知道“这条连接对应的socket变量在哪里”,如果你的代码把这个socket丢了,那信息就发不出去,哪怕客户端还挂在网上。
连接是前提:没有socket句柄,一切都是空谈
很多人写服务器代码时,只用accepted socket接收数据,收完就扔,等到服务器想主动给客户端发消息时,才发现自己手里什么也没有,正确做法是,在accept()返回后,把那个socket对象妥善保存起来,不管是存全局列表、放进字典,还是绑到某个会话对象里,总之你得“留着”。
发送数据的底层动作:send()与write()的区别
在Linux环境下,socket文件描述符本质上也是一个文件描述符,所以write()和send()都能往TCP连接上写数据,区别在于send()带有一个额外的flags参数,比如MSG_DONTWAIT可以控制非阻塞发送,MSG_NOSIGNAL能避免出现SIGPIPE信号导致进程退出。
调用send()之后,数据先进入内核的socket发送缓冲区,然后由内核按TCP协议栈的规则进行分包、重传、流量控制,也就是说,send()成功返回,只代表数据被内核“收下了”,并不代表对方已经收到并读完,这是新手最容易踩的坑,后面会细说。
tcp服务器主动给客户端发消息的两种典型场景
在实际开发里,服务器给客户端发信息一般分成两种场景,理解这两种场景,你就知道什么时候需要在代码里“主动掏连接”。
同步应答客户端问一句,服务器答一句
这是最简单的模式,客户端发送一个请求,服务器在处理完后调用send()把结果发回去,比如一个HTTP请求,客户端发GET,服务器返回HTML,此时发送是“被动触发”的,你不需要保存连接,因为在处理请求的同一个函数里,accept()或read()返回时你就有对应的socket。
这种场景下,不需要管“怎么发”,因为逻辑是顺的:接到请求→处理→发送→关闭。
服务器主动推送先攒着连接,等事件触发时写数据
更常见的需求是:客户端一直连着,但服务器这边可能几分钟都没有主动数据,比如一个监控系统,客户端上线后保持连接,直到服务器检测到异常才主动发一条警报;或者一个聊天服务器,某个用户发消息后,服务器要把这条消息推送给所有在线的好友。
这时候你就不能等着read()返回了,你必须在一开始连接建立后,就把socket存到一个“在线名单”里,然后当某个外部事件发生时,比如数据库变化、定时器到点、另一个客户端发来消息,服务器就遍历这个名单,挨个调用send()。
维护在线连接列表的两种方式
- 简单粗暴型:用一个全局字典,key是客户端唯一标识,value是socket对象,适合客户端数量不多、连接不频繁变动的场景。
- 高效事件型:用select、poll、epoll监听所有socket的可写事件,当某个socket的发送缓冲区空闲了,内核会通知你可以往里写,这种方式适合高并发长连接,但代码复杂度也上来了。
手把手写一个tcp服务端向客户端发送消息的代码示例
准备一个最简单的Python示例,这里不考虑粘包和半包,只演示连接建立后服务器主动发消息,你可以直接在自己的电脑上跑,也可以放到国内云服务器上测试。
服务器端代码
import socket
import time
# 创建服务器socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8888))
server.listen(5)
print("服务器启动,等待客户端连接...")
clients = [] # 保存所有已连接的客户端socket
while True:
server.settimeout(1.0) # 每秒检查一次是否有新连接
try:
conn, addr = server.accept()
print(f"客户端 {addr} 已连接")
clients.append(conn)
except socket.timeout:
pass
# 每隔一段时间,向所有客户端广播消息
now = time.time()
for conn in clients[:]: # 用切片复制,防止列表修改错误
try:
msg = f"当前时间: {time.strftime('%H:%M:%S'
;)}".encode('utf-8')
conn.send(msg)
print(f"已向 {conn.getpeername()} 发送消息")
except Exception as e:
print(f"连接已断开: {e}")
clients.remove(conn)
conn.close()
time.sleep(1)
客户端代码
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888))
while True:
data = client.recv(1024)
if data:
print(data.decode('utf-8'))
运行起来看效果
先启动服务器脚本,再开一个终端运行客户端,你会看到客户端每秒钟打印一次服务器推送的当前时间,这就完成了“tcp服务器主动给客户端发消息”的完整链路。
tcp服务端如何向指定客户端发送数据?核心是“找准连接”
上面是广播式发送,但真实业务里,更多的是“只给某个用户发”,比如你登录了App,服务器给你推了一条专属通知,这时候,你的socket被存到数据库或内存里,服务器从请求中取出你的用户ID,找到对应的socket,然后单独send。
用字典保存每个连接
clients = {} # key: 客户端ID,value: socket
# 当客户端连接时
client_id = receive_login_token(conn) # 伪代码,从登录信息中获取ID
clients[client_id] = conn
# 给指定用户发消息
def send_to_user(user_id, message):
conn = clients.get(user_id)
if conn:
conn.send(message.encode('utf-8'))
给特定客户端发消息的伪代码
假设某聊天室要转发一条群聊消息,但只发给其中的两个好友:
遍历消息中的接收者列表
如果接收者在clients字典里
调用该socket.send()
否则把消息存进离线消息表,等对方上线再补发
这就是“tcp服务端如何向指定客户端发送数据”的标准思路,别想什么神奇技巧,本质就是维护一个“身份ID → socket对象”的映射表。
发送消息时常见的坑:粘包、半包、发送缓冲区
很多程序员问:为什么我send()了,对端收到的数据却是一团乱?这通常不是“发”的问题,而是“读”的问题。
send函数返回不代表对方收到
行业专家指出,TCP协议本身只能保证数据按序到达,但应用层没有“消息边界”的概念,你连续调用两次send(),第一次发了“你好”,第二次发了“世界”,客户端可能在一次recv()里收到“你好世界”,也可能第一次recv()只收到“你好”,第二次才收到“世界”,前者叫粘包,后者叫半包。
粘包和半包的处理思路
- 在应用层定义消息格式:比如每个报文前加4字节长度头,客户端先读长度,再读对应长度的内容。
- 或者用固定分隔符,比如消息以“n”客户端读到换行符就认为一条消息结束。
- 对于高级语言,可以直接用现成的序列化框架,比如protobuf或msgpack,它们自带边界处理。
实际项目中的发送策略:数据量大小与实时性
小数据量直接send,大数据量要拆包
如果一条消息只有几百字节,直接send没任何问题,但如果是几兆的文件,一次send()可能只写入部分数据,内核缓冲区满了就会阻塞,此时你需要循环发送,直到全部写完,常见代码是:
def send_all(conn, data):
length = len(data)
sent = 0
while sent < length:
sent += conn.send(data[sent:])
心跳包与自动重连
服务器给客户端发信息时,最怕客户端已经断开而自己不知道,TCP本身有保活机制,但默认时间太长,业务上一般每30秒发一个心跳包,客户端收到后回一个心跳确认,如果连续几次没收到回复,服务器就把这条连接标记为断开,并从在线列表里移除。
关于tcp服务器给客户端发信息的常见问题
tcp服务器可以同时给多个客户端发信息吗?
可以,每个客户端对应一个独立的socket,服务器遍历在线列表,逐个调用send()即可,注意如果你在多线程环境下操作同一个socket,需要加锁,避免两个线程同时写一个socket导致数据交错。
服务器给客户端发消息时,客户端不在线怎么办?
首先要区分“连接断开”和“业务离线”,如果是socket连接已经close,send()会返回错误,服务端需要移除该连接,如果是用户App退到后台但连接还在,那可以直接发,如果用户彻底退出,业务上应该把消息存到离线表,等用户下次登录后拉取。
tcp服务器怎么给客户端发信息时保证不丢数据?
TCP从协议层面已经保证了传输可靠性,会重传丢失的数据包,但应用层要防止的是“发送缓冲区堆积”和“对端处理不过来”,通常的做法是:发送前检查缓冲区大小,必要时等待可写事件;接收方处理完一条消息后发确认;如果业务重要,再在应用层做一次消息确认和超时重传,没有别的捷径,就是这两层配合。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/748611.html





