在计算机系统中,不同架构对多字节数据的存储顺序存在差异,而网络通信需要统一的字节序标准,这是理解网络编程的重要前提。
大端字节序(Big-Endian)
0x12345678
在内存中存储为12 34 56 78
。小端字节序(Little-Endian)
0x12345678
在内存中存储为78 56 34 12
。netinet/in.h
)uint32_t htonl(uint32_t hostlong); // 主机长整型 → 网络字节序
uint32_t ntohl(uint32_t netlong); // 网络长整型 → 主机字节序
uint16_t htons(uint16_t hostshort); // 主机短整型 → 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络短整型 → 主机字节序
关键作用:端口号(16位)和IP地址(32位)必须通过htons
/htonl
转换为网络字节序后再发送。答:大端序符合人类阅读习惯,且网络协议设计时参考了早期主机(如VAX)的字节序,逐渐成为标准。
套接字地址结构是网络编程中标识通信端点的核心数据结构,分为通用结构和专用结构。
struct sockaddr
)struct sockaddr {
sa_family_t sa_family; // 地址族(如AF_INET、AF_INET6)
char sa_data[14]; // 地址数据(不同协议族格式不同)
};
sa_family
常见值:
AF_INET
:IPv4协议族AF_INET6
:IPv6协议族AF_UNIX
:Unix域套接字(本地进程间通信)struct sockaddr_in
)struct in_addr {
u_int32_t s_addr; // IPv4地址(网络字节序)
};
struct sockaddr_in {
sa_family_t sin_family; // 地址族(AF_INET)
u_int16_t sin_port; // 端口号(网络字节序,需htons转换)
struct in_addr sin_addr; // IPv4地址结构体
};
arpa/inet.h
)inet_addr
:点分十进制字符串 → 网络字节序整数in_addr_t ip = inet_addr("127.0.0.1"); // 返回32位网络字节序整数
inet_ntoa
:网络字节序整数 → 点分十进制字符串struct in_addr addr = {.s_addr = htonl(0x7F000001)};
char* ip_str = inet_ntoa(addr); // 返回"127.0.0.1"
inet_addr
不支持255.255.255.255
以外的广播地址,新代码推荐使用inet_pton
/inet_ntop
(支持IPv6)。Socket编程通过一系列系统调用实现网络通信,核心接口如下:
函数 | 功能 | 关键参数说明 |
---|---|---|
socket() |
创建套接字 | domain :协议族(AF_INET)type :SOCK_STREAM(TCP)/SOCK_DGRAM(UDP) |
bind() |
绑定地址和端口 | addr :套接字地址结构体 |
listen() |
启动监听,创建连接队列 | backlog :最大等待连接数(如5) |
accept() |
接受客户端连接 | 返回新的连接套接字描述符 |
connect() |
客户端发起连接 | serv_addr :服务器地址 |
TCP(面向连接)
ssize_t recv(int sockfd, void *buff, size_t len, int flags); // 读数据
ssize_t send(int sockfd, const void *buff, size_t len, int flags); // 写数据
flags
常用值:0
(阻塞模式)、MSG_DONTWAIT
(非阻塞)。UDP(无连接)
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen); // 读数据并获取发送方地址
ssize_t sendto(int sockfd, const void *buff, size_t len, int flags,
struct sockaddr *dest_addr, socklen_t addrlen); // 写数据并指定接收方地址
close()
:关闭套接字,释放资源。close()
会发送FIN报文,进入四次挥手;UDP直接关闭,无连接释放过程。#include
#include
#include
#include
#include
#include
int main() {
// 1. 创建TCP套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) { perror("socket 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(6000); // 端口号转网络字节序
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 本地回环地址
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed"); exit(1);
}
// 3. 启动监听
if (listen(listen_fd, 5) < 0) {
perror("listen failed"); exit(1);
}
printf("Server listening on 127.0.0.1:6000...\n");
while (1) {
// 4. 接受客户端连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd < 0) {
perror("accept failed");
continue;
}
printf("Client connected: %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 5. 数据交互
char buff[128];
while (1) {
ssize_t n = recv(conn_fd, buff, sizeof(buff)-1, 0);
if (n <= 0) { // 客户端关闭或出错
printf("Client disconnected\n");
break;
}
buff[n] = '\0';
printf("Received: %s\n", buff);
send(conn_fd, "ok", 2, 0); // 发送确认
}
close(conn_fd); // 关闭连接套接字
}
close(listen_fd); // 关闭监听套接字
return 0;
}
#include
#include
#include
#include
#include
#include
int main() {
// 1. 创建TCP套接字
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) { perror("socket 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(6000);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed"); exit(1);
}
printf("Connected to server\n");
// 3. 数据交互
char buff[128];
while (1) {
printf("Enter message (end to quit): ");
fgets(buff, sizeof(buff), stdin);
if (strncmp(buff, "end", 3) == 0) break;
send(sock_fd, buff, strlen(buff)-1, 0); // 发送数据(去除换行符)
ssize_t n = recv(sock_fd, buff, sizeof(buff), 0);
if (n <= 0) {
printf("Server disconnected\n");
break;
}
buff[n] = '\0';
printf("Server response: %s\n", buff);
}
close(sock_fd);
return 0;
}
htons
转换为网络字节序,否则服务器无法正确识别。inet_addr
将字符串转为网络字节序整数,inet_ntoa
反向转换(注意线程不安全,新代码用inet_ntop
)。recv
和accept
默认阻塞,客户端断开时返回n <= 0
,需处理ECONNRESET
等错误码。netstat -natp
实用指南netstat
是排查网络问题的核心工具,-natp
选项用于查看TCP连接状态和端口占用。
netstat -natp
-n
:以数字形式显示IP和端口(不解析域名/服务名)。-a
:显示所有连接(包括监听和已建立)。-t
:仅显示TCP连接。-p
:显示进程PID和名称。字段 | 含义 | 典型值举例 |
---|---|---|
Proto |
协议(TCP/UDP) | TCP |
Local Address |
本地地址和端口 | 127.0.0.1:6000 |
Foreign Address |
远程地址和端口 | 0.0.0.0:0 |
State |
连接状态 | LISTEN(监听)、ESTABLISHED(已建立) |
PID/Program name |
占用端口的进程PID和名称 | 1234/sshd |
close()
调用)。netstat -natp | grep 6000
定位占用端口的进程。CLOSE_WAIT
状态连接数,确认服务器是否漏调close()
。htons
/ntohs
,IP地址(32位)用htonl
/ntohl
。sin_port = 6000;
错误,必须sin_port = htons(6000);
。memset(&addr, 0, sizeof(addr))
初始化结构体,确保未使用字段为0。socket
、bind
、accept
)必须检查返回值,避免静默失败。fcntl
设置套接字为非阻塞模式,配合select
/poll
/epoll
实现多路复用,提升并发能力。setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))
,允许端口快速重用(避免ADDR_INUSE
错误)。网络编程是构建分布式系统的基石,理解字节序、套接字地址结构和核心系统调用是掌握Socket编程的关键。通过服务器-客户端实战代码,可直观感受TCP连接的建立与数据交互过程,而netstat
等工具则是排查网络问题的必备手段。在实际开发中,需注意错误处理、字节序转换和性能优化,确保程序的健壮性和高效性。