一、什么是 TCP 网络编程?
想象你打电话:
- 服务器:接电话的人(先装好电话,等待来电)
- 客户端:打电话的人(知道对方号码,主动拨打)
- TCP:可靠的通话协议(确保对方能听到你说的话)
二、程序功能预览
- 服务器:启动后等待客户端连接,一旦有客户端连上,立即发送 "Hello World!" 消息,然后关闭连接。
- 客户端:连接到服务器,接收服务器发来的消息并打印到屏幕上。
三、服务器端代码详解
c
#include <stdio.h> // 标准输入输出,用于打印错误信息
#include <stdlib.h> // 标准库,用于 exit() 退出程序
#include <string.h> // 字符串操作,如 memset()
#include <unistd.h> // UNIX 标准函数,如 close()、write()
#include <arpa/inet.h> // 提供 IP 地址转换函数,如 htonl()、inet_addr()
#include <sys/socket.h> // 套接字核心函数,如 socket()、bind()、listen()、accept()
// 错误处理函数声明
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock; // 服务器端套接字文件描述符
int clnt_sock; // 与客户端通信的套接字文件描述符
struct sockaddr_in serv_addr; // 服务器地址结构
struct sockaddr_in clnt_addr; // 客户端地址结构(用于保存连接方的信息)
socklen_t clnt_addr_size; // 地址结构大小
char message[] = "Hello World!"; // 要发送的消息
// 检查命令行参数是否正确,需要传入端口号
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// ========== 1. 创建套接字 ==========
// PF_INET: IPv4 协议族
// SOCK_STREAM: 面向连接的 TCP 协议
// 0: 自动选择协议(TCP)
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket() error");
// ========== 2. 初始化服务器地址结构 ==========
memset(&serv_addr, 0, sizeof(serv_addr)); // 清零结构体
serv_addr.sin_family = AF_INET; // 地址族:IPv4
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP 地址:本机任意可用 IP
serv_addr.sin_port = htons(atoi(argv[1])); // 端口:从命令行参数获取,并转为网络字节序
// ========== 3. 绑定套接字到 IP 和端口 ==========
// bind() 将套接字与指定的地址绑定
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
// ========== 4. 监听连接 ==========
// listen() 将套接字变为被动监听状态,等待客户端连接
// 第二个参数是等待队列的最大长度(通常设为 5)
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
// ========== 5. 接受客户端连接 ==========
// accept() 阻塞等待,直到有客户端连接
// 它会返回一个新的套接字(clnt_sock),专门用于与这个客户端通信
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if (clnt_sock == -1)
error_handling("accept() error");
// ========== 6. 发送数据 ==========
// write() 向客户端套接字写入数据
write(clnt_sock, message, sizeof(message));
// ========== 7. 关闭连接 ==========
close(clnt_sock); // 关闭与客户端的连接
close(serv_sock); // 关闭服务器监听套接字
return 0;
}
// 错误处理函数:输出错误信息并退出程序
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
四、客户端代码详解
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock; // 客户端套接字
struct sockaddr_in serv_addr; // 服务器地址结构
char message[30]; // 接收消息的缓冲区
int str_len;
// 检查命令行参数:需要服务器 IP 和端口
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// ========== 1. 创建套接字 ==========
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error");
// ========== 2. 初始化服务器地址结构 ==========
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]); // 将点分十进制 IP 转为整数
serv_addr.sin_port = htons(atoi(argv[2])); // 端口转网络字节序
// ========== 3. 连接服务器 ==========
// connect() 主动连接服务器,参数与 bind() 类似
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
// ========== 4. 接收数据 ==========
// read() 从套接字读取数据,返回值是实际读取的字节数
str_len = read(sock, message, sizeof(message) - 1);
if (str_len == -1)
error_handling("read() error!");
// 确保字符串以 '\0' 结尾
message[str_len] = '\0';
printf("Message from server: %s \n", message);
// ========== 5. 关闭套接字 ==========
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
五、关键函数与参数详解
1. socket() -- 创建套接字
c
int socket(int domain, int type, int protocol);
| 参数 | 常用值 | 说明 |
|---|---|---|
| domain | PF_INET (或 AF_INET) |
IPv4 协议族 |
PF_INET6 |
IPv6 协议族 | |
| type | SOCK_STREAM |
流式套接字,用于 TCP |
SOCK_DGRAM |
数据报套接字,用于 UDP | |
| protocol | 0 |
自动选择协议(TCP 或 UDP) |
返回值:成功返回套接字文件描述符,失败返回 -1。
2. struct sockaddr_in -- 地址结构
c
struct sockaddr_in {
short sin_family; // 地址族,如 AF_INET
unsigned short sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP 地址(网络字节序)
char sin_zero[8]; // 填充,使大小与 sockaddr 一致
};
重要:
sin_port和sin_addr必须使用网络字节序,不能直接用主机字节序的值。- 转换函数:
htons()(host to network short) 用于端口,htonl()(host to network long) 用于 IP。
3. 网络字节序转换函数
| 函数 | 作用 | 示例 |
|---|---|---|
htons() |
16 位主机字节序 → 网络字节序 | htons(8080) |
htonl() |
32 位主机字节序 → 网络字节序 | htonl(INADDR_ANY) |
ntohs() |
网络字节序 → 16 位主机字节序 | ntohs(port) |
ntohl() |
网络字节序 → 32 位主机字节序 | ntohl(addr) |
4. IP 地址转换
| 函数 | 作用 |
|---|---|
inet_addr("192.168.1.1") |
将点分十进制 IP 字符串转为 32 位整数(网络字节序) |
INADDR_ANY |
表示本机任意可用 IP,通常用于服务器绑定时 |
5. bind() -- 绑定地址
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:将套接字与本地 IP 和端口关联。
- 第二个参数要强制转换为
(struct sockaddr*),因为它是通用地址结构。
6. listen() -- 监听连接
c
int listen(int sockfd, int backlog);
- 作用:将套接字变为被动监听状态。
backlog:等待连接队列的最大长度,通常设为 5 或 10。
7. accept() -- 接受连接
c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 作用:阻塞等待客户端连接,返回一个新的套接字用于与客户端通信。
addr和addrlen用于返回客户端的地址信息。
8. connect() -- 主动连接
c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:客户端主动连接服务器。
9. read() / write() -- 数据收发
c
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
- 注意:这两个函数返回实际读写的字节数,可能小于请求的数量(对于流式套接字,这是正常的,需要循环读写)。
六、编译与运行步骤
1. 保存代码
- 将服务器端代码保存为
server.c - 将客户端代码保存为
client.c
2. 编译
bash
gcc server.c -o server
gcc client.c -o client
3. 运行
-
先启动服务器(指定端口,比如 9190):
bash./server 9190此时服务器会阻塞在
accept(),等待客户端连接。 -
再启动客户端(指定服务器 IP 和端口):
bash./client 127.0.0.1 9190客户端连接后,服务器发送 "Hello World!",客户端打印后退出。
4. 预期输出
-
客户端打印:
Message from server: Hello World!
七、常见问题与解答
Q1:为什么 bind() 时要用 htonl(INADDR_ANY)?
A:INADDR_ANY 是一个宏,值为 0。它表示服务器监听本机所有可用 IP 地址。因为网络字节序和主机字节序可能不同,所以必须用 htonl() 转换。
Q2:accept() 返回的套接字和监听套接字有什么区别?
A:监听套接字(serv_sock)只负责接受连接,不负责数据收发。accept() 返回的新套接字(clnt_sock)专门用于和该客户端通信,这样服务器可以同时服务多个客户端。
Q3:为什么 read() 读取后要手动加 \0?
A:read() 读取的数据不保证以 \0 结尾,直接打印可能会导致乱码。所以我们在缓冲区末尾主动添加 \0 来确保字符串正确终止。
Q4:如何让服务器持续服务多个客户端?
A:可以循环调用 accept(),每次接受一个连接后,用 fork() 创建子进程来处理该客户端,父进程继续等待下一个连接。
八、总结
Linux TCP 网络编程的核心流程:
| 步骤 | 服务器 | 客户端 |
|---|---|---|
| 1 | socket() |
socket() |
| 2 | bind() |
初始化地址结构 |
| 3 | listen() |
connect() |
| 4 | accept() |
|
| 5 | write() / read() |
read() / write() |
| 6 | close() |
close() |