【Linux开发】 01 Linux TCP 网络编程——普通服务器

一、什么是 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_portsin_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);
  • 作用:阻塞等待客户端连接,返回一个新的套接字用于与客户端通信。
  • addraddrlen 用于返回客户端的地址信息。

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()
相关推荐
阿kun要赚马内4 小时前
计算机网络:TCP三次握手
网络·tcp/ip·计算机网络
wanhengidc4 小时前
云手机与云真机分别是指什么
服务器·网络·安全·智能手机
Trouvaille ~4 小时前
【项目篇】从零手写高并发服务器(九):HTTP协议支持——从TCP到应用层
linux·服务器·c++·tcp/ip·http·高并发·应用层
HalvmånEver4 小时前
Linux:基于 UDP Socket 的实战项目 --简单双向通信程序
linux·单片机·udp
落羽的落羽4 小时前
【Linux系统】中断机制、用户态与内核态、虚拟地址与页表的本质
java·linux·服务器·c++·人工智能·算法·机器学习
零K沁雪4 小时前
skb_buff 相关函数
linux·内核
!沧海@一粟!11 小时前
麒麟Zabbix Agent安装配置全攻略
linux·服务器·zabbix
qq_4523962312 小时前
【AI 架构师】第十篇:Agent 工业化部署 —— 从 FastAPI 到云端全链路监控
网络·人工智能·ai·fastapi
globaldomain13 小时前
什么是用于长距离高速传输的TCP窗口扩展?
开发语言·网络·php