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)定位故障,培养实操能力。

    • 缓冲区溢出(读写数据超过缓冲区大小)。
相关推荐
Sirens.2 小时前
twikoo:从MongoDB Atlas到本地部署
运维·服务器
qq_三哥啊2 小时前
【mitmproxy】通过 mitmproxy 的HTTP代理模式获取 OpenCode 发起的 AI API 请求的详细信息
网络·http·代理模式
DFT计算杂谈2 小时前
自动化脚本一键绘制三元化合物相图
java·运维·服务器·开发语言·前端·python·自动化
nikolay3 小时前
AI重塑企业信息安全:攻防升级与信任重构
网络·人工智能·网络安全
Yupureki3 小时前
《Linux网络编程》6.UDP原理
linux·运维·服务器·网络·udp
楼田莉子3 小时前
Linux网络:NAT_代理
linux·运维·服务器·开发语言·c++·后端
烛衔溟3 小时前
TypeScript 索引签名、只读数组与 keyof / typeof 入门
linux·ubuntu·typescript
wapicn994 小时前
设置好这一步,让你的SSL证书在到期前自动续期,永不过期
网络·网络协议·ssl
Harvy_没救了4 小时前
【网络运维】 WordPress 部署复盘
运维·网络