本文将带你从零开始掌握C语言Socket编程的核心技术,包含TCP/UDP两种协议的完整实现代码及详细注释
一、Socket编程概述
1.1 什么是Socket?
Socket(套接字)是网络通信的编程接口,它允许不同主机上的进程进行数据交换。可以将其理解为网络通信的端点,就像电话通信中的电话机一样。
1.2 Socket的应用场景
客户端/服务器模型(如Web服务器)
即时通讯软件
网络游戏
分布式系统
远程控制工具
二、核心概念解析
2.1 IP地址与端口
IP地址:设备的网络标识(如
192.168.1.1
)端口号:进程的通信端点(0-65535,其中0-1023为系统保留)
2.2 TCP vs UDP
特性 |
TCP |
UDP |
连接方式 |
面向连接 |
无连接 |
可靠性 |
可靠(重传机制) |
不可靠 |
传输效率 |
较低 |
较高 |
数据顺序 |
保证顺序 |
不保证顺序 |
适用场景 |
文件传输、Web浏览 |
视频流、实时游戏 |
三、Socket编程核心步骤
3.1 TCP通信流程
3.2 UDP通信流程
四、实战代码示例
4.1 TCP服务器实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 配置服务器地址
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 接受任意IP的连接
address.sin_port = htons(PORT); // 端口转换为网络字节序
// 3. 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 4. 开始监听
if (listen(server_fd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("TCP服务器已启动,监听端口:%d\n", PORT);
// 5. 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address,
(socklen_t*)&addrlen)) < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
printf("客户端已连接: %s\n", inet_ntoa(address.sin_addr));
// 6. 接收并回显数据
while (1) {
int valread = read(new_socket, buffer, BUFFER_SIZE);
if (valread <= 0) break;
printf("收到消息: %s\n", buffer);
// 回显给客户端
send(new_socket, buffer, strlen(buffer), 0);
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
// 7. 关闭连接
close(new_socket);
close(server_fd);
return 0;
}
4.2 TCP客户端实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
// 1. 创建socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 配置服务器地址
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 转换IP地址为二进制形式
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
perror("invalid address");
exit(EXIT_FAILURE);
}
// 3. 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
exit(EXIT_FAILURE);
}
printf("已连接到服务器 %s:%d\n", SERVER_IP, PORT);
while (1) {
printf("输入消息 (输入exit退出): ");
fgets(buffer, BUFFER_SIZE, stdin);
// 移除换行符
buffer[strcspn(buffer, "\n")] = 0;
// 检查退出命令
if (strcmp(buffer, "exit") == 0) break;
// 4. 发送数据到服务器
send(sock, buffer, strlen(buffer), 0);
printf("消息已发送\n");
// 5. 接收服务器响应
int valread = read(sock, buffer, BUFFER_SIZE);
printf("服务器回复: %s\n", buffer);
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
// 6. 关闭连接
close(sock);
return 0;
}
4.3 UDP服务器实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr, cliaddr;
// 1. 创建UDP socket
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 配置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 3. 绑定socket
if (bind(sockfd, (const struct sockaddr *)&servaddr,
sizeof(servaddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("UDP服务器已启动,监听端口:%d\n", PORT);
int len, n;
len = sizeof(cliaddr);
while (1) {
// 4. 接收客户端数据
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE,
MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
buffer[n] = '\0';
printf("收到来自 %s 的消息: %s\n",
inet_ntoa(cliaddr.sin_addr), buffer);
// 5. 发送响应
sendto(sockfd, buffer, strlen(buffer),
MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);
printf("已发送响应\n");
}
return 0;
}
五、关键函数深度解析
5.1 socket() - 创建通信端点
int socket(int domain, int type, int protocol);
参数详解:
参数 |
说明 |
|
协议族:AF_INET(IPv4), AF_INET6(IPv6), AF_UNIX(本地通信) |
|
通信类型:SOCK_STREAM(TCP), SOCK_DGRAM(UDP), SOCK_RAW(原始套接字) |
|
通常设为0,由系统自动选择适合的协议 |
注意事项:
创建套接字时不会分配具体地址
原始套接字(SOCK_RAW)需要管理员权限
不同协议族的套接字不能直接通信
5.2 bind() - 绑定地址与端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
地址结构体详解:
struct sockaddr_in {
sa_family_t sin_family; // 地址族: AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IPv4地址
unsigned char sin_zero[8]; // 填充字节
};
struct in_addr {
uint32_t s_addr; // IPv4地址(网络字节序)
};
注意事项:
客户端通常不需要调用bind()
端口号小于1024需要root权限
INADDR_ANY表示接受所有网络接口的连接
地址和端口必须转换为网络字节序(htons/htonl)
5.3 listen() - 设置TCP监听状态
int listen(int sockfd, int backlog);
工作机制:
TCP连接队列
+-----------------------+
| 已完成连接 | 未完成连接 |
+-----------------------+
| |
accept() SYN_RCVD
|
ESTABLISHED
注意事项:
仅用于TCP套接字
backlog参数的实际最大值由系统参数SOMAXCONN决定
调用listen()后套接字变为被动套接字
5.4 accept() - 接受TCP连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
使用示例:
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int client_sock = accept(sockfd, (struct sockaddr*)&cli_addr, &cli_len);
printf("New connection from %s:%d\n",
inet_ntoa(cli_addr.sin_addr),
ntohs(cli_addr.sin_port));
注意事项:
阻塞函数,直到有新连接到达
返回的是一个新的套接字描述符
原始监听套接字继续保持监听状态
5.5 connect() - 建立TCP连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
连接建立过程:
客户端 服务器
|-------- SYN ------------>|
|<------- SYN+ACK --------|
|-------- ACK ------------>|
三次握手完成
注意事项:
客户端调用,用于连接TCP服务器
UDP套接字也可调用connect(),但含义不同
常见错误:
ECONNREFUSED:目标端口无服务
ETIMEDOUT:连接超时
5.6 send()/recv() - TCP数据收发
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
常用flags标志:
标志 |
说明 |
|
默认行为(阻塞模式) |
|
非阻塞操作 |
|
查看数据但不从缓冲区移除 |
关键特性:
TCP保证数据顺序和可靠性
发送缓冲区满时send()可能阻塞
recv()返回0表示连接关闭
5.7 sendto()/recvfrom() - UDP数据收发
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
UDP特点:
无连接:每个数据报独立发送
可能丢失或乱序
单次发送不宜超过MTU(通常1500字节)
适合广播和多播应用
5.8 close() - 关闭套接字
int close(int sockfd);
关闭过程:
TCP关闭序列(四次挥手):
客户端 服务器
|-------- FIN ------------>|
|<------- ACK ------------|
|<------- FIN ------------|
|-------- ACK ------------>|
注意事项:
关闭套接字释放系统资源
使用shutdown()可进行更精细的控制
大量短连接服务中注意TIME_WAIT状态的影响
5.9 地址转换函数
// 字符串IP转二进制网络地址
int inet_pton(int af, const char *src, void *dst);
// 二进制网络地址转字符串IP
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// 主机字节序转网络字节序
uint16_t htons(uint16_t hostshort); // 端口转换
uint32_t htonl(uint32_t hostlong); // IP地址转换
5.10 高级选项设置
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
常用选项:
选项 |
说明 |
SO_REUSEADDR |
允许重用本地地址(解决地址占用问题) |
SO_RCVBUF/SO_SNDBUF |
接收/发送缓冲区大小 |
SO_KEEPALIVE |
启用TCP保活机制 |
TCP_NODELAY |
禁用Nagle算法(减少小数据包延迟) |
六、常见问题及解决方案
6.1 地址已在使用 (Address already in use)
解决方法:
// 在bind()前设置SO_REUSEADDR选项
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
6.2 数据接收不完整
解决方法:
TCP:使用循环接收直到获取完整数据
UDP:确保单次发送不超过MTU(通常1500字节)
6.3 非阻塞Socket
使用fcntl()
设置非阻塞模式:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
6.4 连接超时处理
// 设置连接超时为5秒
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
七、进阶学习建议
多线程/多进程服务器
使用
pthread_create()
创建线程处理多个客户端使用
fork()
创建子进程处理连接
I/O多路复用
select/poll/epoll模型
实现高并发服务器
网络安全
SSL/TLS加密通信
使用OpenSSL库
协议设计
自定义应用层协议
数据序列化(如Protobuf)
八、总结
Socket编程是网络通信的基础,掌握要点:
理解TCP/UDP的核心区别及适用场景
掌握Socket API的使用顺序和参数配置
熟悉网络字节序转换函数(
htonl()
,ntohs()
等)正确处理边界情况和错误代码
逐步扩展为高性能网络程序
本文所有代码已在Ubuntu 22.04/GCC 11.3环境下测试通过,可直接编译运行:
gcc tcp_server.c -o tcp_server && ./tcp_server
gcc tcp_client.c -o tcp_client && ./tcp_client
gcc udp_server.c -o udp_server && ./udp_server
推荐学习资源:
《UNIX网络编程 卷1:套接字联网API》
Beej's Guide to Network Programming(在线免费教程)
Linux man pages(
man socket
查看官方文档)Wireshark网络协议分析工具实践