linux学习笔记 网络编程——Socket入门与TCP客户端/服务器实现

上一节我们学习了网络分层模型(TCP/IP 五层模型),明确了应用层与传输层的核心作用 ------ 应用层负责提供具体网络服务,传输层(TCP/UDP)负责端到端的可靠传输。本节课,我们将基于网络分层的基础,正式进入 Linux 网络编程的核心 ------Socket 编程,理解 Socket 的本质、核心概念和基本操作,最终实现一个简单的 TCP 客户端和服务器程序,完成首次网络通信,为后续复杂网络程序开发打下基础。

核心重点:掌握 Socket 的定义与作用、TCP 通信的核心流程、Linux 中 Socket 相关系统调用,能够独立编写简单的 TCP 客户端和服务器,完成数据的发送与接收。

一、Socket 核心概念(必懂)

在 Linux 网络编程中,Socket(套接字)是应用层与传输层之间的接口,是网络通信的 "端点"------ 简单说,Socket 就是一个 "通信通道",应用程序通过这个通道,向传输层发送数据、接收传输层传来的数据,无需关心底层(网络层、数据链路层、物理层)的传输细节(这些由 Linux 内核的 TCP/IP 协议栈自动处理)。

类比理解:Socket 就像我们打电话时的 "听筒 + 话筒",我们(应用程序)通过听筒听、话筒说,无需关心电话线路(底层传输)如何传递声音信号,只需通过这个 "接口" 完成通话。

1. Socket 的本质

Socket 在 Linux 中本质是一个文件描述符(和我们之前学习的文件、管道的文件描述符一致),Linux 系统遵循 "一切皆文件" 的理念,因此 Socket 的操作(创建、读写、关闭),和文件操作(open、read、write、close)的逻辑完全一致,极大降低了编程复杂度。

核心特点:一个 Socket 由 "IP 地址 + 端口号" 唯一标识,结合传输层协议(TCP/UDP),可唯一确定网络中的一个应用程序通信端点。例如:192.168.1.100:8888(TCP),就代表 192.168.1.100 这台主机上,端口为 8888 的 TCP 应用程序的通信端点。

2. Socket 的类型(重点掌握 2 种)

Linux 中常用的 Socket 类型有两种,对应不同的传输层协议,我们本节课重点学习流式套接字(对应 TCP):

SOCK_STREAM(流式套接字):对应 TCP 协议,面向连接、可靠传输、面向字节流,数据传输有序、无丢失、无重复,适用于文件传输、远程登录等场景(本节课重点实现);

SOCK_DGRAM(数据报套接字):对应 UDP 协议,无连接、不可靠传输、面向数据报,数据传输速度快,可能出现丢失、乱序,适用于视频直播、游戏等实时性要求高的场景(后续章节讲解)。

3. Socket 编程的核心流程(TCP)

TCP 是面向连接的协议,因此 TCP Socket 编程的核心是 "建立连接→通信→关闭连接",客户端和服务器的流程不同,需重点区分(记牢这个流程,编程时直接套用):

服务器端流程(被动接收连接)
  1. 创建 Socket(socket ()):创建一个通信端点,获取 Socket 文件描述符;
  2. 绑定地址(bind ()):将 Socket 与 "IP 地址 + 端口号" 绑定,明确服务器的通信地址;
  3. 监听连接(listen ()):将 Socket 设置为监听状态,等待客户端发起连接;
  4. 接受连接(accept ()):阻塞等待客户端连接,连接成功后,返回一个新的 Socket 文件描述符(用于与该客户端通信);
  5. 读写数据(read ()/write ()):通过新的 Socket,与客户端进行数据交互;
  6. 关闭 Socket(close ()):通信结束后,关闭 Socket,释放资源。
客户端流程(主动发起连接)
  1. 创建 Socket(socket ()):创建通信端点,获取 Socket 文件描述符;
  2. 发起连接(connect ()):向服务器的 "IP 地址 + 端口号" 发起连接请求;
  3. 读写数据(read ()/write ()):连接成功后,与服务器进行数据交互;
  4. 关闭 Socket(close ()):通信结束后,关闭 Socket,释放资源。

关键区别:服务器端需要 "绑定、监听、接受连接" 三个额外步骤,客户端直接发起连接即可;服务器端会有两个 Socket(监听 Socket 和通信 Socket),客户端只有一个通信 Socket。

二、Linux Socket 核心系统调用(实操重点)

编写 TCP Socket 程序,核心是调用 Linux 系统提供的 Socket 相关函数,以下是本节课必备的 5 个系统调用,详细讲解参数含义和使用场景,结合代码示例理解,无需死记硬背,重点掌握用法。

1. 创建 Socket:socket ()

函数原型

<sys/socket.h>int socket(int domain, int type, int protocol);

cpp 复制代码
#### 参数说明
- domain:协议族(地址族),指定Socket的通信范围,Linux中常用AF_INET(IPv4协议,最常用);
- type:Socket类型,本节课用SOCK_STREAM(流式套接字,TCP);
- protocol:协议类型,一般设为0,表示根据前两个参数自动选择对应协议(TCP对应IPPROTO_TCP,UDP对应IPPROTO_UDP)。

#### 返回值
成功:返回一个非负整数(Socket文件描述符);失败:返回-1,设置errno(错误码)。

#### 示例(创建TCP Socket)
```c
// 创建TCP Socket,IPv4协议
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("socket create failed"); // 打印错误信息
    exit(1); // 退出程序
}

2. 绑定地址:bind ()

函数原型
cpp 复制代码
<sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明

sockfd:socket () 函数返回的 Socket 文件描述符;

addr:指向 socket 地址结构的指针,存储要绑定的 "IP 地址 + 端口号",IPv4 对应 struct sockaddr_in(需强制转换为 struct sockaddr *);

addrlen:socket 地址结构的长度,用 sizeof (struct sockaddr_in) 获取。

关键结构:struct sockaddr_in(IPv4 地址结构)
cpp 复制代码
<netinet/in.h>struct sockaddr_in {sa_family_t sin_family; // 协议族,必须设为 AF_INET(IPv4)uint16_t sin_port; // 端口号,需转换为网络字节序(htons () 函数)struct in_addr sin_addr; // IP 地址,需转换为网络字节序(inet_addr () 函数)unsigned char sin_zero [8];// 填充字段,设为 0 即可};

// IP 地址结构(sin_addr 的类型)struct in_addr {in_addr_t s_addr; // 32 位 IPv4 地址,网络字节序};
cpp 复制代码
#### 重要注意:字节序转换
计算机存储数据的字节序有两种:主机字节序(小端,Linux主机默认)和网络字节序(大端,网络通信标准),因此端口号和IP地址必须转换为网络字节序,否则会出现通信失败。
- 端口号转换:htons(port)(host to network short,主机字节序→网络字节序,适用于16位端口号);
- IP地址转换:inet_addr(ip)(将字符串格式的IP地址,转换为网络字节序的32位整数,如"192.168.1.100"→网络字节序);
- 特殊IP:INADDR_ANY(表示绑定本机所有可用IP地址,无需手动指定具体IP,适合服务器)。

#### 示例(绑定IP和端口)
```c
struct sockaddr_in server_addr;
// 初始化地址结构
memset(&server_addr, 0, sizeof(server_addr)); // 清空结构
server_addr.sin_family = AF_INET; // IPv4协议
server_addr.sin_port = htons(8888); // 端口号8888,转换为网络字节序
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定本机所有IP

// 绑定Socket和地址
int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
    perror("bind failed");
    close(sockfd); // 关闭Socket,释放资源
    exit(1);
}

3. 监听连接:listen ()

函数原型
cpp 复制代码
int listen(int sockfd, int backlog);
参数说明

sockfd:绑定后的 Socket 文件描述符;

backlog:监听队列的最大长度,即等待连接的客户端最大数量(一般设为 5、10 即可,具体根据需求调整)。

返回值

成功:返回 0;失败:返回 - 1,设置 errno。

示例(监听连接)
cpp 复制代码
int ret = listen(sockfd, 10); // 监听队列最大长度10
if (ret == -1) {
    perror("listen failed");
    close(sockfd);
    exit(1);
}
printf("服务器已启动,监听端口8888...\n");

4. 接受连接:accept ()

函数原型
cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明

sockfd:监听状态的 Socket 文件描述符;

addr:指向客户端地址结构的指针,用于存储发起连接的客户端的 "IP 地址 + 端口号"(可设为 NULL,表示不关心客户端信息);

addrlen:指向客户端地址结构长度的指针,需先初始化(设为 sizeof (struct sockaddr_in))。

关键特性

accept () 函数是阻塞函数 ------ 如果没有客户端发起连接,程序会一直停在这个函数,直到有客户端连接成功。

返回值

成功:返回一个新的非负整数(通信 Socket 文件描述符,用于与当前客户端通信);失败:返回 - 1,设置 errno。

示例(接受客户端连接)
cpp 复制代码
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 阻塞等待客户端连接
int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_addr_len);
if (connfd == -1) {
    perror("accept failed");
    close(sockfd);
    exit(1);
}
// 打印客户端信息(inet_ntoa()将网络字节序IP转为字符串)
printf("客户端连接成功,IP:%s,端口:%d\n", 
       inet_ntoa(client_addr.sin_addr), 
       ntohs(client_addr.sin_port)); // ntohs():网络字节序→主机字节序

5. 发起连接:connect ()

函数原型(客户端专用)
cpp 复制代码
<sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明

sockfd:客户端 Socket 文件描述符;

addr:指向服务器地址结构的指针,存储服务器的 "IP 地址 + 端口号";

addrlen:服务器地址结构的长度。

关键特性

connect () 函数也是阻塞函数 ------ 如果服务器未响应,程序会一直阻塞,直到连接成功或超时。

返回值

成功:返回 0;失败:返回 - 1,设置 errno(如连接被拒绝、服务器不可达)。

示例(客户端发起连接)
cpp 复制代码
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888); // 服务器端口号
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 服务器IP地址

// 发起连接
int ret = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
    perror("connect failed");
    close(sockfd);
    exit(1);
}
printf("连接服务器成功!\n");

6. 读写数据与关闭 Socket

读写数据

连接成功后,客户端和服务器通过 read () 和 write () 函数读写数据,用法和文件读写完全一致:

c

cpp 复制代码
#include <unistd.h>
// 读数据(从Socket读取数据到缓冲区)
ssize_t read(int fd, void *buf, size_t count);
// 写数据(将缓冲区的数据写入Socket)
ssize_t write(int fd, const void *buf, size_t count);

参数说明:fd 为通信 Socket 文件描述符(服务器用 accept () 返回的 connfd,客户端用 socket () 返回的 sockfd);buf 为数据缓冲区;count 为要读写的字节数。返回值:成功返回实际读写的字节数;失败返回 - 1;read () 返回 0,表示对方关闭连接。

关闭 Socket

通信结束后,用 close () 函数关闭 Socket,释放文件描述符和网络资源:

cpp 复制代码
int close(int fd);

注意:服务器端需关闭两个 Socket(监听 sockfd 和通信 connfd),客户端只需关闭一个 Socket(sockfd)。

三、完整实现:TCP 客户端与服务器程序

结合上述系统调用,我们实现一个简单的 TCP 客户端和服务器:服务器监听 8888 端口,客户端连接服务器后,发送一条消息(如 "Hello, Linux Socket!"),服务器接收消息后,回复一条确认消息(如 "收到消息:XXX"),然后双方关闭连接。

环境说明

Linux 系统(CentOS/Ubuntu 均可)、GCC 编译器,客户端和服务器可在同一台主机(IP 设为 127.0.0.1)或不同主机(需确保网络连通)运行。

1. TCP 服务器程序(server.c)

cpp 复制代码
<sys/socket.h>
#include<netinet/in.h><unistd.h<stdio.h<stdlib.h<string.h>
int main () {
// 1. 创建 TCP Socket
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror ("socket create failed");
exit (1);
}
// 2. 绑定 IP 和端口
struct sockaddr_in server_addr;
memset (&server_addr, 0, sizeof (server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons (8888);
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定本机所有 IP
int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
perror("bind failed");
close(sockfd);
exit(1);
}
// 3. 监听连接
ret = listen (sockfd, 10);
if (ret == -1) {
perror ("listen failed");
close (sockfd);
exit (1);
}
printf ("服务器已启动,监听端口 8888...\n");
// 4. 接受客户端连接(阻塞等待)
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof (client_addr);
int connfd = accept (sockfd, (struct sockaddr*)&client_addr, &client_addr_len);
if (connfd == -1) {
perror ("accept failed");
close (sockfd);
exit (1);
}
printf ("客户端连接成功,IP:% s,端口:% d\n",
inet_ntoa (client_addr.sin_addr),
ntohs (client_addr.sin_port));
// 5. 读写数据
char buf [1024] = {0}; // 数据缓冲区
// 读取客户端发送的消息
ssize_t read_len = read (connfd, buf, sizeof (buf) - 1); // 留 1 个字节存 '\0'
if (read_len == -1) {
perror ("read failed");
close (connfd);
close (sockfd);
exit (1);
} else if (read_len == 0) {
printf ("客户端已关闭连接 \n");
close (connfd);
close (sockfd);
return 0;
}
// 打印客户端消息
printf ("收到客户端消息:% s\n", buf);
// 回复客户端消息
char reply [1024] = {0};
sprintf (reply, "收到消息:% s", buf);
ssize_t write_len = write (connfd, reply, strlen (reply));
if (write_len == -1) {
perror ("write failed");
close (connfd);
close (sockfd);
exit (1);
}
printf ("已回复客户端消息 \n");
// 6. 关闭 Socket
close (connfd); // 关闭与客户端的通信 Socket
close (sockfd); // 关闭监听 Socket
printf ("服务器已关闭连接 \n");
return 0;
}
cpp 复制代码
### 2. TCP客户端程序(client.c)
```c
#include <sys/socket.h>
<netinet/in.h<unistd<stdio<stdlib<string.h>

int main() {
    // 1. 创建TCP Socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket create failed");
        exit(1);
    }

    // 2. 发起连接(连接服务器)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888); // 服务器端口号
    // 服务器IP地址(同一台主机用127.0.0.1,不同主机填服务器实际IP)
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (ret == -1) {
        perror("connect failed");
        close(sockfd);
        exit(1);
    }
    printf("连接服务器成功!\n");

    // 3. 读写数据
    char msg[] = "Hello, Linux Socket!";
    // 发送消息给服务器
    ssize_t write_len = write(sockfd, msg, strlen(msg));
    if (write_len == -1) {
        perror("write failed");
        close(sockfd);
        exit(1);
    }
    printf("已发送消息给服务器:%s\n", msg);

    // 读取服务器的回复
    char buf[1024] = {0};
    ssize_t read_len = read(sockfd, buf, sizeof(buf) - 1);
    if (read_len == -1) {
        perror("read failed");
        close(sockfd);
        exit(1);
    } else if (read_len == 0) {
        printf("服务器已关闭连接\n");
        close(sockfd);
        return 0;
    }
    printf("收到服务器回复:%s\n", buf);

    // 4. 关闭Socket
    close(sockfd);
    printf("客户端已关闭连接\n");

    return 0;
}

四、编译与运行(实操步骤)

1. 编译程序

在 Linux 终端中,进入代码所在目录,执行以下命令编译服务器和客户端程序(GCC 编译器):

bash 复制代码
# 编译服务器程序,生成可执行文件server
gcc server.c -o server

# 编译客户端程序,生成可执行文件client
gcc client.c -o client

编译成功后,目录下会生成两个可执行文件:server(服务器)和 client(客户端)。

2. 运行程序(注意顺序)

排查与解决

3. 读写数据失败(read/write failed)

原因
排查与解决

六、学习小结

本节课实现的是 "单客户端" 的 TCP 服务器,只能处理一个客户端的连接,下一节我们将优化服务器,实现多客户端并发处理(使用多线程或 IO 复用),让服务器能够同时响应多个客户端的请求。

  1. 先启动服务器:

    bash 复制代码
    ./server

    服务器启动后,会打印 "服务器已启动,监听端口 8888...",进入阻塞状态,等待客户端连接。

  2. 再启动客户端(打开新的终端):

    bash 复制代码
    ./client

    3. 预期运行结果

    服务器终端输出:
    bash 复制代码
    服务器已启动,监听端口8888...
    客户端连接成功,IP:127.0.0.1,端口:12345(端口随机)
    收到客户端消息:Hello, Linux Socket!
    已回复客户端消息
    服务器已关闭连接

    客户端终端输出:

    bash 复制代码
    连接服务器成功!
    已发送消息给服务器:Hello, Linux Socket!
    收到服务器回复:收到消息:Hello, Linux Socket!
    客户端已关闭连接

    五、常见问题与排查(重点)

    编写和运行 Socket 程序时,容易出现以下问题,结合网络分层知识和 Linux 命令排查,重点掌握排查思路:

    1. 绑定失败(bind failed: Address already in use)

    原因

    端口号已被其他程序占用(如之前运行的服务器未正常关闭,端口未释放)。

    排查与解决
    bash 复制代码
    # 查看8888端口的占用情况
    lsof -i:8888
    # 或
    ss -tuln | grep 8888
    
    # 杀死占用端口的进程(pid为进程ID,从上述命令中获取)
    kill -9 pid
    
    # 或修改服务器端口号(如改为8889),重新编译运行

    2. 连接失败(connect failed: Connection refused)

    原因
  3. 服务器未启动;

  4. 服务器 IP 地址或端口号填写错误;

  5. 服务器和客户端网络不通(如防火墙阻挡)。

  6. 确认服务器已启动,且监听的端口号正确;

  7. 用 ping 命令测试服务器和客户端的网络连通性(如 ping 127.0.0.1);

  8. 关闭 Linux 防火墙(临时测试用):systemctl stop firewalld。

  9. 对方已关闭 Socket(read 返回 0);

  10. Socket 文件描述符错误(如未创建成功、已关闭);

  11. 检查 Socket 创建、绑定、连接是否成功;

  12. 确保读写时使用的是正确的 Socket 文件描述符(服务器用 connfd,客户端用 sockfd);

  13. 增大缓冲区大小(如将 buf 设为 2048 字节)。

  14. Socket 是应用层与传输层的接口,本质是 Linux 文件描述符,操作逻辑与文件一致;

  15. TCP Socket 编程的核心流程:服务器(创建→绑定→监听→接受→读写→关闭),客户端(创建→连接→读写→关闭);

  16. 重点掌握 5 个核心系统调用:socket ()、bind ()、listen ()、accept ()、connect (),以及 read ()、write ()、close () 的使用;

  17. 字节序转换(htons ()、inet_addr ())是关键,必须将端口和 IP 转换为网络字节序,否则会通信失败;

  18. 掌握常见问题的排查方法,结合 Linux 命令(lsof、ss、ping)定位故障,培养实操能力。

    • 缓冲区溢出(读写数据超过缓冲区大小)。
相关推荐
律宏阔2 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔2 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴2 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千2 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei372 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei372 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei372 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW2 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
百万蹄蹄向前冲2 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js
GeW2 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux