技术演进中的开发沉思-100 Linux服务编程系列: 网络编程基础 API (上)

在 Linux 环境下做网络开发,基础 API 是绕不开的 “基本功”—— 从地址处理到 socket 创建,再到连接建立,每一步都依赖这套标准化接口。对程序员来说,掌握这些 API 不仅是 “会调用”,更要理解其背后的设计逻辑(比如字节序转换、地址结构差异),避免因细节疏漏导致的隐蔽 bug。下面按 “地址处理→socket 操作→连接交互” 的流程,拆解今天的核心内容。

一、socket 地址 API

在调用 socket 相关函数前,必须先搞定 “地址”—— 就像寄快递要写清收件人地址,网络通信中要明确 “数据发给谁”“从哪发”,socket 地址 API 就是用来处理这些 “地址信息” 的工具集,核心围绕字节序、地址结构、IP 转换展开。

1.1 主机字节序和网络字节序

这是网络编程的 “第一坑”—— 不同主机的字节序可能不同(比如 x86 架构是小端序,PowerPC 是大端序),若直接传输数据,会出现 “数据错乱”(比如 0x1234 在小端机存为 0x3412,大端机存为 0x1234,直接传会被解析成不同数值)。

Linux 提供了 4 个核心转换函数,程序员必须熟练掌握:

  • htonl():将 32 位主机字节序(host)转换为网络字节序(network),常用于 IP 地址转换(比如将本地存储的 IP 数值转成网络传输格式);
  • htons():将 16 位主机字节序转换为网络字节序,常用于端口号转换(端口是 16 位数据);
  • ntohl()/ntohs():反向转换,将网络字节序转为主机字节序,用于接收数据后解析 IP、端口。

实操中,只要涉及 “IP + 端口” 的传输或存储,必须做字节序转换。比如用inet_addr()将字符串 IP(如 “192.168.1.100”)转成 32 位整数后,要调用htonl()转成网络字节序,再填入 socket 地址结构;接收数据时,从地址结构中取出的 IP 整数,需用ntohl()转为主机字节序,再用inet_ntoa()转成字符串供人阅读。

1.2 通用 socket 地址

socket 支持多种协议(TCP、UDP、UNIX 域协议等),每种协议的地址格式不同,因此 Linux 设计了struct sockaddr通用地址结构,作为所有协议地址的 “统一接口”—— 函数参数中用它接收不同协议的地址,内部再强制转换为对应专用结构。

其定义如下(简化版):

struct sockaddr {

sa_family_t sa_family; // 地址族(如AF_INET表示IPv4,AF_UNIX表示UNIX域)

char sa_data[14]; // 地址数据,不同协议格式不同

};

对程序员来说,sockaddr的核心作用是 “类型兼容”—— 比如bind()、connect()等函数的地址参数类型是struct sockaddr *,无论传入 IPv4 还是 UNIX 域的地址,都要先强制转换为该类型。但实际使用中,很少直接操作sa_data(14 字节可能不够存 IPv6 地址),更多是用专用地址结构,再通过强制转换适配函数参数。

1.3 专用 socket 地址

最常用的是 IPv4 专用地址结构struct sockaddr_in,专门存储 IPv4 协议的 “IP + 端口” 信息,解决了struct sockaddr数据长度不足的问题:

struct sockaddr_in {

sa_family_t sin_family; // 地址族,固定为AF_INET

in_port_t sin_port; // 端口号,需用htons()转网络字节序

struct in_addr sin_addr; // IPv4地址结构

unsigned char sin_zero[8]; // 填充字段,使结构大小与sockaddr一致,通常设为0

};

// IPv4地址结构

struct in_addr {

in_addr_t s_addr; // 32位IPv4地址,需用htonl()转网络字节序

};

实操中,程序员会先初始化struct sockaddr_in:设置sin_family为AF_INET,sin_port为htons(8080)(比如指定 8080 端口),sin_addr.s_addr为htonl(INADDR_ANY)(表示绑定所有本地 IP),再将其强制转换为struct sockaddr *传给bind()函数。

此外,IPv6 对应struct sockaddr_in6,UNIX 域对应struct sockaddr_un,使用逻辑类似 —— 根据协议选择对应的专用结构,初始化后适配通用地址类型。

1.4 IP 地址转换函数

程序员习惯用字符串表示 IP(如 “192.168.1.100”),但 socket 地址结构中需要 32 位整数(IPv4),因此 Linux 提供了两组核心转换函数:

  1. 字符串转整数(人→机)

实操推荐用inet_aton(),比如:

struct in_addr addr;

if (inet_aton("192.168.1.100", &addr) == 0) {

perror("inet_aton error"); // 处理无效IP

}

  • inet_addr(const char *cp):将点分十进制字符串 IP 转成 32 位无符号整数,失败返回INADDR_NONE(注意:无法处理 “255.255.255.255”,因该值与INADDR_NONE相同);
  • net_aton(const char *cp, struct in_addr *inp):功能更完善,成功返回非 0,失败返回 0,将转换后的整数存入inp->s_addr,支持所有合法 IPv4 地址。
  1. 整数转字符串(机→人)

调试时常用inet_ntoa(),比如:

struct sockaddr_in client_addr;

// 从accept()获取客户端地址后,打印客户端IP

printf("client IP: %s\n", inet_ntoa(client_addr.sin_addr));
  • inet_ntoa(struct in_addr in):将 32 位整数 IP 转成点分十进制字符串,返回静态缓冲区地址(注意:线程不安全,多线程需用inet_ntop());
  • inet_ntop(int af, const void *src, char *dst, socklen_t size):支持 IPv4/IPv6,将src(如struct in_addr *)转成字符串存入dst,size指定dst缓冲区大小,线程安全。

二、socket 创建

在 Linux 中,socket 本质是 “网络版文件描述符”—— 通过socket()函数创建,后续的连接、读写操作都通过该描述符进行,就像操作文件用文件描述符一样。

2.1 函数原型与参数解析

int socket(int domain, int type, int protocol);
  • domain:地址族,指定协议族,如AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(UNIX 域);
  • type:socket 类型,决定通信特性:
  • SOCK_STREAM:流式 socket,对应 TCP 协议(可靠、面向连接、字节流);
  • SOCK_DGRAM:数据报 socket,对应 UDP 协议(不可靠、无连接、数据报);
  • protocol:具体协议,通常设为 0,表示根据domain和type自动选择(如AF_INET+SOCK_STREAM自动选 TCP,AF_INET+SOCK_DGRAM自动选 UDP)。

2.2 实操示例与错误处理

创建 TCP socket 的代码示例:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd == -1) {

perror("socket error"); // 打印错误原因(如权限不足、协议不支持)

exit(EXIT_FAILURE);

}

程序员必须检查返回值 ——socket()失败返回 - 1,常见错误包括:domain指定无效地址族、type与domain不兼容(如AF_UNIX+SOCK_RAW)、系统资源不足(socket 描述符耗尽)。

创建成功后,sockfd是一个非负整数,后续的bind()、listen()、connect()等操作都依赖它,使用完需调用close(sockfd)释放资源,避免内存泄漏。

三、命名 socket

创建 socket 后,它还没有 “身份标识”(地址 + 端口),bind()函数的作用就是 “命名”—— 将 socket 与特定的 IP 和端口绑定,让其他进程能通过该地址找到它(类似给电话装个固定号码)。

3.1 函数原型与参数解析

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:socket()创建的 socket 描述符;
  • addr:指向struct sockaddr类型的地址结构(实际传入专用结构,如struct sockaddr_in);
  • addrlen:地址结构的大小(如sizeof(struct sockaddr_in))。

3.2 实操示例与关键注意事项

TCP 服务器绑定 8080 端口的代码:


struct sockaddr_in server_addr;

memset(&server_addr, 0, sizeof(server_addr)); // 初始化清零

server_addr.sin_family = AF_INET; // IPv4

server_addr.sin_port = htons(8080); // 端口转网络字节序

// 绑定所有本地IP(INADDR_ANY = 0.0.0.0),也可指定具体IP(如inet_aton转换后的地址)

server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

// 调用bind()

if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {

perror("bind error");

close(sockfd);

exit(EXIT_FAILURE);

}

程序员需重点关注两个问题:

  • 端口号冲突:若 8080 端口已被其他进程占用,bind()会返回 - 1,错误码为EADDRINUSE,解决方法是换端口,或设置SO_REUSEADDR选项(允许端口复用,需在bind()前调用setsockopt());
  • IP 地址有效性:若指定的 IP 不是本机已配置的 IP(如本机无 192.168.1.200,却绑定该 IP),bind()会返回 - 1,错误码为EADDRNOTAVAIL,需确保绑定的 IP 在本机网卡上存在。

对客户端而言,通常不需要主动bind()—— 调用connect()时,系统会自动分配一个空闲端口和本地 IP,减少程序员的配置工作;但特殊场景(如客户端需固定端口)也可手动bind()。

四、监听 socket

bind()给 socket 绑定地址后,TCP 服务器需要调用listen()函数,将 socket 从 “主动连接态” 转为 “被动监听态”—— 就像打开电话的 “来电等待” 功能,准备接收客户端的连接请求。

4.1 函数原型与参数解析

int listen(int sockfd, int backlog);
  • sockfd:bind()后的 socket 描述符(必须是SOCK_STREAM类型,即 TCP socket);
  • backlog:监听队列的最大长度 ——TCP 连接建立需三次握手,客户端发送SYN后,连接会进入 “半连接队列”(SYN 队列),三次握手完成后进入 “全连接队列”(accept 队列),backlog表示全连接队列的最大长度(不同系统实现可能有差异,如 Linux 下实际长度是backlog + 1)。

4.2 实操示例与核心逻辑

TCP 服务器监听的代码:

// backlog设为10,表示全连接队列最多存10个已完成三次握手的连接

if (listen(sockfd, 10) == -1) {

perror("listen error");

close(sockfd);

exit(EXIT_FAILURE);

}

printf("server listening on 0.0.0.0:8080...\n");

调用listen()后,sockfd变成 “监听 socket”(被动 socket),只能用于accept()接收连接,不能直接用于读写数据;后续客户端的connect()请求,会被该 socket 接收并加入监听队列。

程序员需注意backlog的设置:过小会导致全连接队列满时,新的连接请求被拒绝(客户端收到ECONNREFUSED错误);过大则会占用更多系统资源,通常根据业务并发量设置(如 Web 服务器设为 128 或 256)。

五、接受连接

listen()让服务器进入监听状态后,accept()函数的作用是 “从全连接队列中取出一个已完成三次握手的连接”,并创建一个新的 socket 描述符 —— 该新描述符用于与客户端进行数据读写,而原监听 socket 继续等待其他客户端连接。

5.1 函数原型与参数解析

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:listen()后的监听 socket 描述符;
  • addr:输出参数,用于存储客户端的地址信息(如struct sockaddr_in),若不需要客户端地址,可设为 NULL;
  • addrlen:输入输出参数,传入时表示addr缓冲区的大小(如sizeof(struct sockaddr_in)),输出时表示客户端地址的实际大小。

5.2 实操示例与关键细节

TCP 服务器接受连接的代码:

struct sockaddr_in client_addr;

socklen_t client_addrlen = sizeof(client_addr);

// 阻塞等待客户端连接(默认是阻塞模式)

int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);

if (connfd == -1) {

perror("accept error");

// 注意:accept()失败时,监听socket仍有效,通常不退出,继续重试

return -1;

}

// 打印客户端信息

printf("client connected: IP=%s, Port=%d\n",

inet_ntoa(client_addr.sin_addr),

ntohs(client_addr.sin_port)); // 端口转主机字节序

程序员需掌握三个核心点:

  1. 阻塞特性:默认情况下,accept()是阻塞函数 —— 若全连接队列为空,函数会一直阻塞,直到有新连接到来;若需非阻塞模式,需通过fcntl()或ioctl()将监听 socket 设为非阻塞,此时accept()无连接时会返回 - 1,错误码为EAGAIN或EWOULDBLOCK。
  1. 新 socket 的作用:connfd是 “连接 socket”(主动 socket),专门用于与当前客户端通信(读写数据),每个客户端连接对应一个独立的connfd;通信完成后需调用close(connfd)释放资源,否则会导致文件描述符泄漏。
  1. addrlen的使用:必须传入指针类型,若传入值类型,accept()无法修改其值,可能导致内存越界;若客户端地址结构实际大小小于传入的addrlen,accept()会将addrlen设为实际大小。

六、发起连接

对 TCP 客户端而言,无需bind()(系统自动分配地址)和listen(),创建 socket 后直接调用connect()函数,向服务器发起连接请求 —— 三次握手的发起端就是connect(),调用成功表示三次握手完成,客户端与服务器建立起可靠连接。

6.1 函数原型与参数解析

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:socket()创建的客户端 socket 描述符(SOCK_STREAM类型);
  • addr:指向服务器地址结构的指针(如struct sockaddr_in,需填入服务器的 IP 和端口);
  • addrlen:服务器地址结构的大小(如sizeof(struct sockaddr_in))。

6.2 实操示例与错误处理

TCP 客户端连接服务器的代码:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

if (sockfd == -1) {

perror("socket error");

exit(EXIT_FAILURE);

}

// 初始化服务器地址

struct sockaddr_in server_addr;

memset(&server_addr, 0, sizeof(server_addr));

server_addr.sin_family = AF_INET;

server_addr.sin_port = htons(8080); // 服务器端口

// 将服务器IP字符串转成整数

if (inet_aton("192.168.1.100", &server_addr.sin_addr) == 0) {

perror("invalid server IP");

close(sockfd);

exit(EXIT_FAILURE);

}

// 发起连接

if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {

perror("connect error");

close(sockfd);

exit(EXIT_FAILURE);

}

printf("connected to server successfully\n");

客户端程序员需重点处理connect()的错误场景:

  • 连接超时:服务器无响应(如网络不通、服务器未启动),connect()会阻塞一段时间后返回 - 1,错误码为ETIMEDOUT;
  • 连接被拒绝:服务器存在但端口未监听(如listen()未调用)或全连接队列满,connect()返回 - 1,错误码为ECONNREFUSED;
  • 地址不可达:服务器 IP 不存在或路由不可达,错误码为EHOSTUNREACH或ENETUNREACH。

与服务器不同,客户端的connect()成功后,sockfd可直接用于读写数据(无需新创建描述符),通信完成后调用close(sockfd)关闭连接。

最后小结:TCP 通信的 API 调用流程

站在程序员视角,本章 API 的核心价值是构建 TCP 通信的 “标准化流程”,以 “服务器 - 客户端” 为例,完整调用链如下:

服务器端流程

  • socket():创建 TCP socket 描述符(sockfd);
  • bind():将 sockfd 与本地 IP、端口绑定;
  • listen():将 sockfd 设为监听态,开启连接等待;
  • accept():从监听队列取出客户端连接,创建连接 socket(connfd);
  • read()/write():通过 connfd 与客户端读写数据;
  • close(connfd):关闭当前客户端连接;
  • close(sockfd):关闭监听 socket,停止服务。

客户端流程

  • socket():创建 TCP socket 描述符(sockfd);
  • connect():向服务器 IP、端口发起连接;
  • read()/write():通过 sockfd 与服务器读写数据;
  • close(sockfd):关闭连接。

这些 API 是 Linux 网络编程的 “基石”,后续的并发服务器(如多进程、多线程、IO 多路复用)、UDP 通信,都是在这个基础上的扩展。未完待续..........

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值