1 核心概念
1.1 套接字 (Socket)
- 定义:Socket 是网络通信的端点 (Endpoint)。在 Linux 内核中,Socket 被抽象为一种文件。
- 本质:应用程序通过 Socket 文件描述符与内核的网络协议栈进行交互。
- 分类 :
- 流式套接字 (SOCK_STREAM) :基于 TCP 协议。提供面向连接、可靠、无差错、无重复、按序到达的字节流传输。
- 数据报套接字 (SOCK_DGRAM) :基于 UDP 协议。提供无连接、不可靠、固定最大长度的数据报传输。
1.2 网络字节序 (Network Byte Order)
- 问题背景 :不同 CPU 架构对多字节整数的存储方式不同。
- 小端序 (Little-Endian):低位字节存放在低地址(x86/ARM 常为此类)。
- 大端序 (Big-Endian):高位字节存放在低地址。
- 标准规定 :TCP/IP 网络传输协议统一规定使用 大端序 (Big-Endian)。
- 转换函数 :
htons()(Host to Network Short): 16位主机序转网络序(常用于端口)。htonl()(Host to Network Long): 32位主机序转网络序(常用于IP)。ntohs()/ntohl(): 网络序转主机序。
1.3 IP 地址与端口
- IP 地址:用于在网络层唯一标识一台主机(IPv4 为 32 位整数)。
- 端口 (Port):用于在传输层区分同一主机上的不同应用程序进程(16 位整数,范围 0-65535)。
2 关键数据结构
需包含头文件:#include <netinet/in.h>
实际上,这些结构体做了比较复杂的封装,下面出现的形式是简化后的。
2.1 通用地址结构体
这是 Linux 网络 API (如 bind, connect) 使用的通用参数类型。所有具体的协议地址结构体都必须强制类型转换为此类型。
c
struct sockaddr {
unsigned short sa_family; // 地址族 (如 AF_INET)
char sa_data[14]; // 协议地址数据 (混合了IP和端口)
};
2.2 IPv4 专用地址结构体
这是在代码中实际填充的结构体,填充完毕后强转为 struct sockaddr * 传给 API。
c
struct sockaddr_in {
short int sin_family; // 地址族 (必须设为 AF_INET)
unsigned short int sin_port; // 端口号 (必须是网络字节序, 使用 htons)
struct in_addr sin_addr; // IP 地址结构体
unsigned char sin_zero[8]; // 填充字节 (必须置0,为了与 struct sockaddr 对齐)
};
// 其中 IP 地址结构体定义如下:
struct in_addr {
unsigned long s_addr; // 32位 IP 地址 (网络字节序)
};
常用赋值模式:
c
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = INADDR_ANY; // 自动绑定本机所有网卡 IP
// 或者指定 IP: inet_pton(AF_INET, "192.168.1.10", &addr.sin_addr);
3 核心 API
需包含头文件:#include <sys/socket.h>
3.1 创建套接字
c
int socket(int domain, int type, int protocol);
- 功能:创建一个通信端点并返回一个描述符。
- 参数 :
domain:协议族。AF_INET (IPv4), AF_INET6 (IPv6), AF_UNIX (本地 IPC)。type:套接字类型。SOCK_STREAM (TCP), SOCK_DGRAM (UDP)。protocol:具体协议,通常传 0(系统根据 type 自动选择)。
- 返回值:成功返回非负文件描述符 (fd),失败返回 -1 并设置 errno。
3.2 绑定地址(服务器端)
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:将套接字与特定的本地地址 (IP + Port) 关联。
- 注意 :
addr参数需要将struct sockaddr_in*强转为struct sockaddr*。 - 返回值:成功返回 0,失败返回 -1。
3.3 监听连接(服务器端)
c
int listen(int sockfd, int backlog);
- 功能 :将套接字标记为被动 (Passive) 状态,用于接受传入的连接请求。
- 参数 :
backlog:挂起连接队列的最大长度(即三次握手完成但尚未被accept取走的连接数)。
- 返回值:成功返回 0,失败返回 -1。
3.4 接受连接(服务器端)
c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能 :从监听队列中取出第一个连接请求。若队列为空,默认为阻塞 (Blocking) 等待。
- 参数 :
addr:(传出参数) 用于存储客户端的地址信息。addrlen:(传入传出参数) 传入addr的大小,函数返回实际写入的大小。
- 返回值 :
- 成功:返回一个新的文件描述符 (Connected Socket),专门用于与该客户端通信。
- 失败:返回 -1。
- 重要区分 :
sockfd是监听套接字(总机),返回值是已连接套接字(分机)。
3.5 发起连接(客户端)
c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:向指定地址的服务器发起 TCP 三次握手。
- 返回值:成功返回 0,失败返回 -1(如超时、连接被拒)。
3.6 数据传输
在 Linux 中,Socket 就是文件,因此可以使用系统 I/O。
write(fd, buf, len):发送数据。read(fd, buf, len):接收数据。- 返回
> 0:实际读到的字节数。 - 返回
0:表示对端关闭了连接 (EOF)。这是判断断线的标准方式。 - 返回
< 0:发生错误 (errno)。
- 返回
4 地址转换函数
需包含头文件:#include <arpa/inet.h>
- 将字符串 IP (如 "192.168.1.1") 转换为二进制网络字节序结构(Presentation to Network)
c
int inet_pton(int af, const char *src, void *dst);
- 将二进制网络字节序 IP 转换为字符串(Network to ASCII)
c
char *inet_ntoa(struct in_addr in);
5 标准 TCP 编程模型
| 阶段 | 服务器端 (Server) 动作 | 客户端 (Client) 动作 |
|---|---|---|
| 1. 准备 | socket() 创建套接字 |
socket() 创建套接字 |
| 2. 地址 | 填充 sockaddr_in (本机 IP+端口) |
填充 sockaddr_in (服务器 IP+端口) |
| 3. 绑定 | bind() 绑定地址 |
(客户端通常不需要 bind,系统自动分配随机端口) |
| 4. 等待 | listen() 开启监听 |
|
| 5. 建立 | accept() 阻塞等待连接 |
connect() 发起连接 (三次握手) |
| 6. 通信 | read() / write() |
write() / read() |
| 7. 结束 | close() (先关分机,再关总机) |
close() |

6 TCP Echo 服务器实验
- Server (泰山派):开启 8888 端口监听,收到什么就回发什么(Echo)。
- Client (PC/虚拟机):连接泰山派,发送字符串。
1 编写服务器代码
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main()
{
int server_fd, new_socket;
struct sockaddr_in address; // IPv4 地址结构体
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
printf("[Server] Creating socket...\n");
// 1. 创建 Socket
// AF_INET: IPv4
// SOCK_STREAM: TCP (如果是 UDP 则用 SOCK_DGRAM)
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)
{
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 绑定 (Bind) IP 和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听本机所有网卡 (Wi-Fi, 网口)
address.sin_port = htons(PORT); // 注意:端口号必须转为网络字节序(Big Endian)
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 监听 (Listen)
// 3: 待处理连接队列的最大长度
if (listen(server_fd, 3) < 0)
{
perror("listen");
exit(EXIT_FAILURE);
}
printf("[Server] Listening on port %d... Waiting for connection.\n", PORT);
// 4. 接受连接 (Accept) - 会阻塞在这里,直到有客户端连上来
// accept 返回一个新的 fd (new_socket) 专门用于和这个客户端通信
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0)
{
perror("accept");
exit(EXIT_FAILURE);
}
// 打印客户端的 IP
printf("[Server] Connection accepted from %s\n", inet_ntoa(address.sin_addr));
// 5. 循环通信
while (1)
{
memset(buffer, 0, BUFFER_SIZE);
// 读取数据
int valread = read(new_socket, buffer, BUFFER_SIZE);
if (valread <= 0)
{
// read 返回 0 表示客户端断开了连接
printf("[Server] Client disconnected.\n");
break;
}
printf("[Server] Received: %s", buffer);
// 回显数据 (Echo)
char msg[BUFFER_SIZE + 32];
sprintf(msg, "Echo: %s", buffer);
send(new_socket, msg, strlen(msg), 0);
}
// 6. 关闭连接
close(new_socket); // 关闭与客户端的连接
close(server_fd); // 关闭服务器监听
return 0;
}
2 编写客户端代码
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main(int argc, char const *argv[])
{
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
if (argc != 2)
{
printf("Usage: %s <Server_IP>\n", argv[0]);
return -1;
}
// 1. 创建 Socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 2. 将 IP 字符串 (如 "192.168.1.10") 转为二进制网络格式
if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0)
{
printf("\nInvalid address / Address not supported \n");
return -1;
}
printf("[Client] Connecting to %s:%d...\n", argv[1], PORT);
// 3. 连接服务器 (Connect)
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("\nConnection Failed \n");
return -1;
}
printf("[Client] Connected! Type text to send.\n");
// 4. 发送数据
while (1)
{
printf("> ");
memset(buffer, 0, BUFFER_SIZE);
// 从键盘读取输入
fgets(buffer, BUFFER_SIZE, stdin);
// 发送
send(sock, buffer, strlen(buffer), 0);
// 接收回显
memset(buffer, 0, BUFFER_SIZE);
int valread = read(sock, buffer, BUFFER_SIZE);
if (valread > 0)
{
printf("[Server Reply]: %s\n", buffer);
}
else
{
printf("Server disconnected.\n");
break;
}
}
close(sock);
return 0;
}
3 编译与运行
交叉编译 Server (给泰山派),编译 Client (给 PC/Ubuntu),联网运行:
-
查询板子 IP: 在板子上输入
ifconfig或ip addr,找到wlan0(Wi-Fi) 或eth0(网线) 的 IP 地址。 假设是192.168.31.200。 -
启动 Server (板子端):
bash
# ./tcp_server
[Server] Creating socket...
[Server] Listening on port 8888... Waiting for connection.
- 启动 Client (PC端):
bash
$ ./main 192.168.31.200
[Client] Connecting to 192.168.173.65:8888...
[Client] Connected! Type text to send.
-
现象。
-
在 PC 输入
Hello,板子终端会打印[Server] Received: Hello。 -
PC 终端会收到
[Server Reply]: Echo: Hello。 -
如果在 PC 按
Ctrl+C退出,板子会检测到read返回 0,并打印[Server] Client disconnected.。
-