第二讲:网络编程基础(一)——Socket编程入门,TCP与UDP的选择
好,咱们正式开始聊网络编程。
说实话,很多新手一上来就被各种网络模型、IO复用给吓住了。其实没必要。网络编程的根基,就是Socket。你把这个搞明白了,后面什么epoll、reactor,都是锦上添花。
这一讲,我带你把Socket编程的底裤扒干净。顺便聊聊那个老生常谈的问题:TCP还是UDP?
2.1 什么是Socket?
Socket,中文叫“套接字”。名字挺拗口,但本质很简单——它就是操作系统提供的一个编程接口,让你能在网络上收发数据。
你可以把它想象成一个“电话插座”。你这边插上电话机(绑定IP和端口),拨号(连接),然后就能通话(收发数据)了。挂断(关闭连接),就这么回事。
我当年刚学的时候,总觉得Socket是个很玄乎的东西。后来写了个简单的聊天程序,才恍然大悟:哦,原来就是读写一个文件描述符啊!
2.2 TCP Socket编程——面向连接的可靠传输
TCP,全称传输控制协议。它的核心特点就四个字:可靠、有序。
你发出去的数据,TCP保证能按顺序到达对方手里。如果丢了,它会重传。如果乱了,它会排序。你只管把数据扔给它,剩下的它帮你搞定。
嗯,听起来很美好。但代价是什么?性能开销大,有连接建立和断开的成本。
2.2.1 TCP Socket的典型流程
TCP Socket编程,服务端和客户端的流程是不一样的。我画个简单的步骤图给你看:
- 服务端: socket() → bind() → listen() → accept() → recv()/send() → close()
- 客户端: socket() → connect() → send()/recv() → close()
你看,服务端多了一个bind和listen。为什么?因为服务端需要明确告诉操作系统:“我在这台机器的这个端口上等着,谁来我都接。”
客户端不需要。客户端是主动发起连接的,操作系统会自动给它分配一个临时端口。
2.2.2 一个最简单的TCP回射服务器
光说不练假把式。咱们直接上代码。这是一个最简单的TCP回射服务器——客户端发什么,服务端原样返回什么。
// 服务端代码(C++风格,但核心是系统调用)
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main() {
// 1. 创建Socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
return -1;
}
// 2. 绑定地址和端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
server_addr.sin_port = htons(8888); // 端口8888
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(listen_fd);
return -1;
}
// 3. 开始监听
if (listen(listen_fd, 10) == -1) { // 10是backlog,表示最大等待连接数
perror("listen");
close(listen_fd);
return -1;
}
printf("服务器已启动,监听端口8888...\n");
// 4. 接受客户端连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd == -1) {
perror("accept");
close(listen_fd);
return -1;
}
printf("客户端已连接:%s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 5. 收发数据
char buffer[1024];
while (1) {
memset(buffer, 0, sizeof(buffer));
int n = recv(conn_fd, buffer, sizeof(buffer) - 1, 0);
if (n <= 0) {
printf("客户端断开连接\n");
break;
}
printf("收到:%s", buffer);
send(conn_fd, buffer, n, 0); // 原样返回
}
// 6. 关闭连接
close(conn_fd);
close(listen_fd);
return 0;
}
2.3 UDP Socket编程——无连接的不可靠传输
UDP,用户数据报协议。它和TCP完全相反——不可靠、无序、无连接。
你发一个数据包出去,它可能到,也可能不到。到了也可能顺序是乱的。UDP不负责这些,它只管发。
听起来很坑?其实不然。UDP的优势是:速度快、开销小、支持广播和多播。
2.3.1 UDP Socket的流程
UDP的流程比TCP简单得多:
- 服务端: socket() → bind() → recvfrom()/sendto() → close()
- 客户端: socket() → sendto()/recvfrom() → close()
看到了吗?UDP不需要listen和accept,也不需要connect。它就像寄信——你写好地址(IP和端口),扔进邮筒(sendto),就行了。对方能不能收到,看运气。
2.3.2 一个最简单的UDP回射服务器
// UDP回射服务器
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main() {
// 1. 创建Socket(注意是SOCK_DGRAM)
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd == -1) {
perror("socket");
return -1;
}
// 2. 绑定地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8888);
if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(sock_fd);
return -1;
}
printf("UDP服务器已启动,监听端口8888...\n");
// 3. 收发数据(不需要accept)
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
while (1) {
memset(buffer, 0, sizeof(buffer));
int n = recvfrom(sock_fd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&client_addr, &client_len);
if (n < 0) {
perror("recvfrom");
break;
}
printf("收到来自 %s:%d 的消息:%s",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 原样返回
sendto(sock_fd, buffer, n, 0,
(struct sockaddr*)&client_addr, client_len);
}
close(sock_fd);
return 0;
}
2.4 TCP vs UDP:到底该怎么选?
这是每个游戏后端开发者都会面临的问题。我直接给你一个决策指南:
| 场景 | 推荐协议 | 原因 |
|---|---|---|
| 登录、注册、支付 | TCP | 必须可靠,丢一个包都不行 |
| 聊天消息 | TCP | 需要保证消息顺序和完整性 |
| 实时位置同步 | UDP | 可以容忍丢包,但延迟要低 |
| 帧同步(格斗/赛车) | UDP | 每帧数据独立,丢了就丢了 |
| 状态同步(MMO) | TCP或UDP+可靠层 | 看具体实现,各有优劣 |
| 广播/组播 | UDP | TCP不支持广播 |
我个人习惯是:核心逻辑用TCP,高频状态用UDP。
举个例子。我在做一款MOBA游戏时,英雄的移动、技能释放这些高频操作,用的就是UDP。为什么?因为TCP一旦丢包重传,整个操作序列就卡住了,玩家会感觉“技能放不出来”。而UDP丢了就丢了,下一帧补上就行。
但玩家的金币、等级、装备这些关键数据,必须用TCP。这些数据错了,游戏就没法玩了。
2.5 几个容易踩的坑
最后,分享几个我当年踩过的坑。你记住了,能省不少时间。
- 端口被占用: 服务端关闭后,端口会进入TIME_WAIT状态,短时间内无法重用。可以用setsockopt设置SO_REUSEADDR来解决。
- TCP粘包: TCP是流式协议,没有消息边界。你发两次数据,对方可能一次就收完了。解决办法:自己定义消息格式,比如“长度+内容”。
- UDP丢包: UDP不保证送达。如果你的应用场景需要可靠性,要么自己实现ACK和重传,要么直接用TCP。
- 缓冲区溢出: recv和recvfrom的缓冲区要足够大。UDP一个包最大65507字节,但建议不要超过1472(以太网MTU 1500 - IP头20 - UDP头8)。
好了,这一讲就到这里。Socket编程其实不难,难的是把它用好、用对。下一讲,我们会深入聊聊TCP的底层机制——三次握手、四次挥手、滑动窗口。这些东西搞明白了,你才算真正入了网络编程的门。
记住:纸上得来终觉浅,绝知此事要躬行。把上面的代码敲一遍,跑起来,比看十遍文章都管用。