上一节我们学习了网络分层模型(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 编程的核心是 "建立连接→通信→关闭连接",客户端和服务器的流程不同,需重点区分(记牢这个流程,编程时直接套用):
服务器端流程(被动接收连接)
- 创建 Socket(socket ()):创建一个通信端点,获取 Socket 文件描述符;
- 绑定地址(bind ()):将 Socket 与 "IP 地址 + 端口号" 绑定,明确服务器的通信地址;
- 监听连接(listen ()):将 Socket 设置为监听状态,等待客户端发起连接;
- 接受连接(accept ()):阻塞等待客户端连接,连接成功后,返回一个新的 Socket 文件描述符(用于与该客户端通信);
- 读写数据(read ()/write ()):通过新的 Socket,与客户端进行数据交互;
- 关闭 Socket(close ()):通信结束后,关闭 Socket,释放资源。
客户端流程(主动发起连接)
- 创建 Socket(socket ()):创建通信端点,获取 Socket 文件描述符;
- 发起连接(connect ()):向服务器的 "IP 地址 + 端口号" 发起连接请求;
- 读写数据(read ()/write ()):连接成功后,与服务器进行数据交互;
- 关闭 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 复用),让服务器能够同时响应多个客户端的请求。
-
先启动服务器:
bash./server服务器启动后,会打印 "服务器已启动,监听端口 8888...",进入阻塞状态,等待客户端连接。
-
再启动客户端(打开新的终端):
bash./client3. 预期运行结果
服务器终端输出:
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)
原因
-
服务器未启动;
-
服务器 IP 地址或端口号填写错误;
-
服务器和客户端网络不通(如防火墙阻挡)。
-
确认服务器已启动,且监听的端口号正确;
-
用 ping 命令测试服务器和客户端的网络连通性(如 ping 127.0.0.1);
-
关闭 Linux 防火墙(临时测试用):systemctl stop firewalld。
-
对方已关闭 Socket(read 返回 0);
-
Socket 文件描述符错误(如未创建成功、已关闭);
-
检查 Socket 创建、绑定、连接是否成功;
-
确保读写时使用的是正确的 Socket 文件描述符(服务器用 connfd,客户端用 sockfd);
-
增大缓冲区大小(如将 buf 设为 2048 字节)。
-
Socket 是应用层与传输层的接口,本质是 Linux 文件描述符,操作逻辑与文件一致;
-
TCP Socket 编程的核心流程:服务器(创建→绑定→监听→接受→读写→关闭),客户端(创建→连接→读写→关闭);
-
重点掌握 5 个核心系统调用:socket ()、bind ()、listen ()、accept ()、connect (),以及 read ()、write ()、close () 的使用;
-
字节序转换(htons ()、inet_addr ())是关键,必须将端口和 IP 转换为网络字节序,否则会通信失败;
-
掌握常见问题的排查方法,结合 Linux 命令(lsof、ss、ping)定位故障,培养实操能力。
- 缓冲区溢出(读写数据超过缓冲区大小)。