Socket(套接字)是程序与网络之间的接口。一个程序当服务器、监听端口,另一个当客户端、主动连上来,两边用 socket 收发数据,聊天、网页、流媒体背后全是这套。下面讲 C 语言里 socket 怎么建、怎么连、怎么传数据。
什么是 Socket
Socket 是 IP 地址 + 端口号组合出来的一个网络端点,比如 192.168.1.1:8080。客户端和服务器各持一个 socket,通过它们交换数据。
按传输协议分两类:
- TCP Socket(流式):面向连接、可靠,数据按序到达,像打电话。
- UDP Socket(数据报):无连接、快但可能丢包,像发快递。
下面先用一个最小例子感受全流程,服务器收一句"Hello from client",回一句"Hello from server"。
服务器先 socket→bind→listen→accept 等着,客户端 socket→connect 连上来,之后两边互发数据,最后都 close。
完整示例:TCP 服务器与客户端
server.c:建服务器
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8080
int main(int argc, char const* argv[]) {
int server_fd, new_socket;
ssize_t valread;
struct sockaddr_in address;
int opt = 1;
socklen_t addrlen = sizeof(address);
char buffer[1024] = { 0 };
char* hello = "Hello from server";
/* 1. 创建 socket 文件描述符 */
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
/* 2. 允许地址端口复用,避免重启时报 address already in use */
if (setsockopt(server_fd, SOL_SOCKET,
SO_REUSEADDR | SO_REUSEPORT, &opt,
sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
/* 3. 绑定地址和端口 */
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");
exit(EXIT_FAILURE);
}
/* 5. 接受一个客户端的连接 */
if ((new_socket = accept(server_fd, (struct sockaddr*)&address,
&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
/* 6. 读客户端发来的数据,再回一句 */
valread = read(new_socket, buffer, 1024 - 1);
printf("%s\n", buffer);
send(new_socket, hello, strlen(hello), 0);
printf("Hello message sent\n");
/* 7. 关闭连接和监听 socket */
close(new_socket);
close(server_fd);
return 0;
}client.c:连服务器
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8080
int main(int argc, char const* argv[]) {
int status, valread, client_fd;
struct sockaddr_in serv_addr;
char* hello = "Hello from client";
char buffer[1024] = { 0 };
/* 1. 创建 socket */
if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
/* 2. 把字符串形式的 IP 转成二进制 */
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
/* 3. 主动连接服务器 */
if ((status = connect(client_fd, (struct sockaddr*)&serv_addr,
sizeof(serv_addr))) < 0) {
printf("\nConnection Failed \n");
return -1;
}
/* 4. 发数据、收回复 */
send(client_fd, hello, strlen(hello), 0);
printf("Hello message sent\n");
valread = read(client_fd, buffer, 1024 - 1);
printf("%s\n", buffer);
/* 5. 关闭连接 */
close(client_fd);
return 0;
}编译并运行:
gcc server.c -o server
gcc client.c -o client
./server & # 终端 1 先跑服务器
./client # 终端 2 跑客户端输出:
客户端:Hello message sent
Hello from server
服务器:Hello from client
Hello message sent服务器端七步走
- socket():创建 socket,拿到一个文件描述符。
- setsockopt()(可选):设置选项,比如 SO_REUSEADDR 避免"address already in use"。
- bind():把 socket 绑到指定 IP 和端口。
- listen():进入被动监听,等待客户端连接;backlog 是等待队列长度。
- accept():从队列里取出一个连接请求,返回一个新的 socket 描述符用于通信。
- send() / recv():用新 socket 收发数据。
- close():通信完毕关闭 socket,释放资源。
核心调用签名:
sockfd = socket(domain, type, protocol);
setsockopt(sockfd, level, optname, optval, optlen);
bind(sockfd, (struct sockaddr*)&addr, addrlen);
listen(sockfd, backlog);
new_socket = accept(sockfd, (struct sockaddr*)&addr, &addrlen);
send(sockfd, buf, len, flags);
recv(sockfd, buf, len, flags);
close(fd);其中 domain 常用 AF_INET(IPv4)、AF_INET6(IPv6);type 用 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP);protocol 一般传 0 表示让系统选默认。
客户端四步走
- socket():和服务器一样先建 socket。
- connect():用服务器地址和端口发起连接,指定 sockaddr 里存服务器的 IP 和端口。
- send() / recv():和服务器收发数据。
- close():通信完毕关闭。
UDP 与 TCP 的差异
| 对比项 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接,先 connect | 无连接,直接发数据 |
| 可靠性 | 可靠、按序、无重复 | 可能丢包、乱序 |
| 速度 | 较慢(有握手、确认) | 较快 |
| 典型场景 | 网页、文件传输、聊天 | 音视频、游戏、实时查询 |
UDP 不需要 bind 监听,直接 sendto() / recvfrom() 带上目标地址就能收发。
常见问题与排查
- 连接失败:确认客户端连的 IP 和端口与服务器绑定的一致。
- 端口占用:bind 报错"address already in use",换端口或等旧进程释放,也可用 SO_REUSEADDR。
- 阻塞卡住:socket 默认是阻塞的,accept()/recv() 没有连接或数据时会一直等。需要时可把 socket 设成非阻塞。
socket 编程的核心就七个系统调用:socket、bind、listen、accept、send、recv、close。服务器把这七步走一遍,客户端走简化版,网络通信就跑起来了。