当前位置: 首页 > news >正文

Socket编程核心API详解

目录

一、创建套接字

1、函数原型

2、参数说明

1. domain(协议族/地址族)

2. type(套接字类型)

3. protocol(协议类型)

3、返回值

4、示例代码

1. 创建 TCP 套接字

2. 创建 UDP 套接字

3. 创建原始套接字(需 root 权限)

5、关键注意事项

6、底层原理

7、常见问题

二、绑定端口号(服务器端)

1、函数原型

2、参数说明

1. sockfd(套接字描述符)

2. addr(套接字地址结构体)

struct sockaddr_in(IPv4)

struct sockaddr_in6(IPv6)

struct sockaddr(通用地址结构)

3. addrlen(地址结构体长度)

3、返回值

4、示例代码

1. 绑定 IPv4 TCP 套接字

2. 绑定 IPv6 UDP 套接字

5、关键注意事项

1. 地址格式转换

2. 特殊地址

3. 端口占用(EADDRINUSE)

4. 权限问题(EACCES)

5. 后续操作

6、常见问题

Q1:bind() 失败时如何调试?

Q2:INADDR_ANY 和 127.0.0.1 的区别?

Q3:为什么 bind() 需要 addrlen?

7、总结

三、监听套接字(TCP服务器)

1、函数原型

2、参数说明

1. sockfd(套接字描述符)

2. backlog(等待连接队列的最大长度)

3、返回值

4、工作流程

5、关键注意事项

1. 监听队列的行为

2. 野蛮绑定(未调用 bind())

3. backlog 的实际限制

4. 监听套接字 vs 连接套接字

6、常见问题

Q1:listen() 和 accept() 的关系?

Q2:如何避免连接队列溢出?

Q3:listen() 后能否继续调用 bind()?

Q4:UDP 需要 listen() 吗?

7、示例代码

8、总结

四、接收连接请求(TCP服务器)

1、函数原型

2、参数说明

1. sockfd(监听套接字描述符)

2. addr(客户端地址结构体指针)

3. addrlen(地址长度指针)

3、返回值

4、工作流程

5、关键注意事项

1. 监听套接字 vs 连接套接字

2. 阻塞与非阻塞模式

3. 多线程/多进程处理

4. 地址结构体的兼容性

5. 错误处理

6、常见问题

Q1:accept() 返回的新套接字与监听套接字的关系?

Q2:如果队列中没有连接,accept() 会怎样?

Q3:如何获取客户端的 IP 和端口?

Q4:accept() 和 connect() 的关系?

Q5:能否多次调用 accept() 处理多个客户端?

7、示例代码(完整服务器)

8、总结

五、建立连接(TCP客户端)

1、函数原型

2、参数说明

1. sockfd(套接字描述符)

2. addr(服务器地址结构体指针)

3. addrlen(地址结构体长度)

3、返回值

4、工作流程

5、关键注意事项

1. 阻塞与非阻塞模式

2. 错误处理

3. 地址结构体的兼容性

4. 连接后的套接字行为

5. 多次调用 connect()

6、常见问题

Q1:connect() 失败后能否重试?

Q2:如何设置连接超时?

Q3:connect() 和 bind() 的关系?

Q4:如何获取连接后的本地地址和端口?

Q5:connect() 对 UDP 的作用?

7、示例代码(完整客户端)

8、总结

六、完整TCP服务器和客户端示例

TCP服务器

TCP客户端

七、总结


Socket编程是网络通信的基础,它提供了一组API用于在不同主机或同一主机的不同进程之间建立网络连接。下面我将详细讲解Socket编程中的常见API:

一、创建套接字

1、函数原型

    int socket(int domain, int type, int protocol) 是 Unix/Linux 系统中用于创建套接字(socket)的核心系统调用函数。它是网络通信和进程间通信的基础,用于创建一个新的套接字描述符。以下是对该函数的详细讲解:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

2、参数说明

1. domain(协议族/地址族)

指定套接字使用的通信协议族(即地址格式),常见的值包括:

  • AF_INET:IPv4 互联网协议族(如 TCP/UDP over IPv4)。

  • AF_INET6:IPv6 互联网协议族。

  • AF_UNIX(或 AF_LOCAL):本地进程间通信(通过文件系统路径)。

  • AF_PACKET:直接访问底层网络接口(如链路层数据包)。

  • AF_NETLINK:用户空间与内核通信(如路由表操作)。

注意AF_(Address Family)和 PF_(Protocol Family)在历史上可能混用,但现代代码中通常统一使用 AF_

2. type(套接字类型)

指定套接字的通信语义,常见的值包括:

  • SOCK_STREAM:面向连接的可靠字节流(如 TCP)。提供双向、有序、无重复的数据传输。

  • SOCK_DGRAM:无连接的数据报服务(如 UDP)。不可靠,但开销小。

  • SOCK_RAW:原始套接字,允许直接操作网络层协议(如自定义 IP 头)。

  • SOCK_SEQPACKET:面向连接的固定长度有序数据报。

  • SOCK_RDM:可靠但无序的数据报(较少用)。

3. protocol(协议类型)

指定具体的协议,通常设为 0(自动选择默认协议)。例如:

  • 若 domain=AF_INET 且 type=SOCK_STREAM,默认协议是 IPPROTO_TCP

  • 若 domain=AF_INET 且 type=SOCK_DGRAM,默认协议是 IPPROTO_UDP

其他常见协议:

  • IPPROTO_TCP:TCP 协议。

  • IPPROTO_UDP:UDP 协议。

  • IPPROTO_ICMP:ICMP 协议(用于 ping)。

注意:某些 domain 和 type 组合可能只支持特定协议(如 SOCK_RAW 必须显式指定协议)。

3、返回值

  • 成功:返回一个非负整数,表示套接字描述符(类似文件描述符,用于后续操作如 bind()listen()connect() 等)。

  • 失败:返回 -1,并设置 errno 表示错误原因(如 EACCES 权限不足、ENOMEM 内存不足等)。

4、示例代码

#include <sys/socket.h>
#include <iostream>int main() {// 创建TCP套接字int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);if (tcp_socket == -1) {std::cerr << "创建TCP套接字失败" << std::endl;return -1;}std::cout << "TCP套接字创建成功,描述符: " << tcp_socket << std::endl;// 创建UDP套接字int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);if (udp_socket == -1) {std::cerr << "创建UDP套接字失败" << std::endl;close(tcp_socket);return -1;}std::cout << "UDP套接字创建成功,描述符: " << udp_socket << std::endl;close(tcp_socket);close(udp_socket);return 0;
}

1. 创建 TCP 套接字

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_socket == -1) {perror("socket creation failed");exit(EXIT_FAILURE);
}

2. 创建 UDP 套接字

int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_socket == -1) {perror("socket creation failed");exit(EXIT_FAILURE);
}

3. 创建原始套接字(需 root 权限)

int raw_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (raw_socket == -1) {perror("raw socket creation failed");exit(EXIT_FAILURE);
}

5、关键注意事项

  • 协议兼容性:并非所有 domain 和 type 的组合都有效。例如,AF_INET + SOCK_SEQPACKET 可能无效。

  • 原始套接字权限:创建 SOCK_RAW 套接字通常需要 root 权限(因可能绕过内核协议栈)。

  • 资源释放:使用 close() 关闭套接字描述符以释放资源。

  • 错误处理:始终检查返回值,并通过 errno 诊断错误(如 perror() 或 strerror(errno))。

  • 后续操作:创建套接字后,通常需要:

    • 服务器端:bind() → listen() → accept()

    • 客户端:connect()

6、底层原理

  • 套接字是内核提供的通信端点,通过文件描述符抽象。

  • 调用 socket() 时,内核会分配内核数据结构(如 struct socket 和 struct sock),并返回描述符供用户空间操作。

7、常见问题

Q:protocol=0 的含义?

A:表示根据 domain 和 type 自动选择默认协议(如 TCP 对应 SOCK_STREAM)。

Q:AF_INET 和 PF_INET 的区别?

A:历史上 PF_ 表示协议族,AF_ 表示地址族,但 POSIX 规范中两者应一致,通常混用无害。

Q:如何创建非阻塞套接字?

A:创建后通过 fcntl(fd, F_SETFL, O_NONBLOCK) 设置非阻塞标志。

通过理解 socket() 的参数和行为,可以灵活实现各种网络通信场景(如 HTTP 服务器、UDP 广播、自定义协议等)。


二、绑定端口号(服务器端)

1、函数原型

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 是 Unix/Linux 网络编程中用于将套接字(socket)绑定到特定地址和端口的系统调用函数。它是服务器端程序初始化套接字的关键步骤之一。以下是对该函数的详细讲解:

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2、参数说明

1. sockfd(套接字描述符)

  • 作用:由 socket() 创建的套接字描述符。

  • 示例

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

2. addr(套接字地址结构体)

  • 作用:指向一个 struct sockaddr 类型的指针,用于指定绑定的地址和端口。

  • 常见结构体

    • struct sockaddr_in(用于 IPv4)

    • struct sockaddr_in6(用于 IPv6)

    • struct sockaddr_un(用于 Unix 域套接字)

struct sockaddr_in(IPv4)
struct sockaddr_in {sa_family_t    sin_family;   // 地址族(AF_INET)in_port_t      sin_port;     // 16 位端口号(网络字节序)struct in_addr sin_addr;     // 32 位 IPv4 地址(网络字节序)char           sin_zero[8];  // 未使用(填充为 0)
};struct in_addr {uint32_t s_addr;  // 32 位 IPv4 地址(网络字节序)
};
struct sockaddr_in6(IPv6)
struct sockaddr_in6 {sa_family_t     sin6_family;   // 地址族(AF_INET6)in_port_t       sin6_port;     // 16 位端口号(网络字节序)uint32_t        sin6_flowinfo; // IPv6 流量信息struct in6_addr sin6_addr;     // 128 位 IPv6 地址uint32_t        sin6_scope_id; // 接口 ID(用于链路本地地址)
};struct in6_addr {unsigned char s6_addr[16];  // 128 位 IPv6 地址
};
struct sockaddr(通用地址结构)
struct sockaddr {sa_family_t sa_family;  // 地址族(如 AF_INET、AF_INET6)char        sa_data[14]; // 地址数据(具体格式取决于 sa_family)
};

注意bind() 接受 struct sockaddr*,但实际使用时通常转换为 struct sockaddr_in* 或 struct sockaddr_in6*

3. addrlen(地址结构体长度)

  • 作用:指定 addr 指向的结构体的长度(字节数)。

  • 计算方式

    • IPv4:sizeof(struct sockaddr_in)

    • IPv6:sizeof(struct sockaddr_in6)

    • Unix 域套接字:sizeof(struct sockaddr_un)

3、返回值

  • 成功:返回 0

  • 失败:返回 -1,并设置 errno(如 EADDRINUSE 地址已占用,EACCES 权限不足)。

4、示例代码

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>int main() {int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {std::cerr << "创建套接字失败" << std::endl;return -1;}// 设置服务器地址结构struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口server_addr.sin_port = htons(8080);      // 设置端口号为8080// 绑定套接字到指定端口if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {std::cerr << "绑定端口失败" << std::endl;close(server_fd);return -1;}std::cout << "成功绑定到端口8080" << std::endl;close(server_fd);return 0;
}

1. 绑定 IPv4 TCP 套接字

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in server_addr;// 1. 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 设置地址和端口memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);  // 端口 8080(网络字节序)server_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有本地 IP(0.0.0.0)// 3. 绑定套接字if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}printf("Bind successful!\n");close(sockfd);return 0;
}

2. 绑定 IPv6 UDP 套接字

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in6 server_addr;sockfd = socket(AF_INET6, SOCK_DGRAM, 0);if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}memset(&server_addr, 0, sizeof(server_addr));server_addr.sin6_family = AF_INET6;server_addr.sin6_port = htons(8080);  // 端口 8080server_addr.sin6_addr = in6addr_any;  // 监听所有 IPv6 地址(::)if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}printf("Bind successful!\n");close(sockfd);return 0;
}

5、关键注意事项

1. 地址格式转换

将本机的主机字节序(可能是大端或者小端),转化为网络字节序(为大端)!!!(这个要记住!!!)

  • htons():主机字节序 → 网络字节序(用于端口号)。

  • htonl():主机字节序 → 网络字节序(用于 IPv4 地址,如 INADDR_ANY)。

  • inet_pton():字符串 IP → 二进制格式(如 "192.168.1.1" → in_addr)。

2. 特殊地址

  • INADDR_ANY(IPv4):绑定到所有本地 IP(0.0.0.0)。

  • in6addr_any(IPv6):绑定到所有 IPv6 地址(::)。

  • 127.0.0.1:仅允许本地回环访问。

3. 端口占用(EADDRINUSE

  • 如果端口已被占用,bind() 会失败。

  • 解决方法:

    • 使用 setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, ...) 允许端口重用。

    • 确保之前的进程已释放端口(如调用 close())。

4. 权限问题(EACCES

  • 在 Linux 上,绑定 1024 以下的端口需要 root 权限。

5. 后续操作

  • TCP 服务器bind() → listen() → accept()

  • UDP 服务器bind() → recvfrom()

  • 客户端:通常不需要 bind(),除非需要指定本地端口。

6、常见问题

Q1:bind() 失败时如何调试?

检查 errno

if (bind(...) == -1) {perror("bind error");printf("Error code: %d\n", errno);
}

常见错误:

  • EADDRINUSE:端口已被占用。

  • EACCES:权限不足。

  • EINVAL:套接字已绑定或未正确初始化。

Q2:INADDR_ANY 和 127.0.0.1 的区别?

  • INADDR_ANY0.0.0.0):监听所有网络接口(包括外网和内网)。

  • 127.0.0.1:仅允许本地回环访问(外部无法连接)。

Q3:为什么 bind() 需要 addrlen

  • 因为 struct sockaddr 是通用结构,addrlen 告诉内核 addr 的具体格式(IPv4/IPv6/Unix 域套接字)。

7、总结

  • bind() 用于将套接字与特定 IP 和端口绑定。

  • 关键步骤:

    1. 创建套接字(socket())。

    2. 填充 struct sockaddr_in 或 struct sockaddr_in6

    3. 调用 bind()

  • 错误处理和地址格式转换是常见难点。

通过正确使用 bind(),服务器程序可以监听指定的网络接口和端口,为后续的 listen() 和 accept() 做准备。


三、监听套接字(TCP服务器)

1、函数原型

    int listen(int sockfd, int backlog) 是 Unix/Linux 网络编程中用于 将套接字设置为被动监听状态 的系统调用函数,主要用于 TCP 服务器。它是服务器端程序初始化套接字的关键步骤之一,通常在 bind() 之后、accept() 之前调用。以下是对该函数的详细讲解:

#include <sys/socket.h>
int listen(int sockfd, int backlog);

2、参数说明

1. sockfd(套接字描述符)

作用:由 socket() 创建并已绑定(bind())的套接字描述符。

要求

  • 必须是 面向连接的套接字(如 SOCK_STREAM,即 TCP)。

  • 必须已经调用 bind() 绑定了本地地址和端口(除非是野蛮绑定,见下文)。

示例

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(sockfd, backlog);  // 开始监听

2. backlog(等待连接队列的最大长度)

作用:指定 未完成连接队列(SYN_RCVD 状态) 和 已完成连接队列(ESTABLISHED 状态) 的总和上限。

详细解释

  • 未完成连接队列:客户端已发送 SYN,但服务器尚未完成三次握手(SYN_RCVD 状态)。

  • 已完成连接队列:三次握手已完成,等待 accept() 取走(ESTABLISHED 状态)。

历史与现代实现

  • 传统实现backlog 直接限制队列长度。

  • Linux 2.2+backlog 是 已完成连接队列 的上限,未完成连接队列由内核参数 net.ipv4.tcp_max_syn_backlog 控制。

  • 默认值:通常为 128(Linux)或 SOMAXCONN(系统最大值,可通过 cat /proc/sys/net/core/somaxconn 查看)。

设置建议

  • 高并发服务器可设为 SOMAXCONN(如 listen(sockfd, SOMAXCONN))。

  • 过小会导致连接被拒绝(ECONNREFUSED),过大会占用更多内存。

3、返回值

  • 成功:返回 0

  • 失败:返回 -1,并设置 errno(如 EBADF 无效套接字,ENOTSOCK 非套接字描述符)。

4、工作流程

  1. 创建套接字socket())。

  2. 绑定地址和端口bind())。

  3. 设置为监听状态listen())。

  4. 接受客户端连接accept())。

// 典型 TCP 服务器监听流程
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr = {.sin_family = AF_INET,.sin_port = htons(8080),.sin_addr.s_addr = INADDR_ANY
};
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(sockfd, SOMAXCONN);  // 开始监听

5、关键注意事项

1. 监听队列的行为

三次握手与队列

  • 客户端 connect() 触发三次握手。

  • 服务器在收到 SYN 后,将连接放入 未完成队列SYN_RCVD)。

  • 完成三次握手后,连接移至 已完成队列ESTABLISHED),等待 accept()

队列溢出

  • 如果两个队列的总和超过 backlog,新连接会被 静默丢弃(客户端收到 ECONNREFUSED)。

  • 可通过 netstat -ntp 或 ss -ltp 查看监听队列状态。

2. 野蛮绑定(未调用 bind()

如果未调用 bind()listen() 会触发 隐式绑定

  • 内核自动分配一个临时端口(通常从 32768 开始)。

  • 适用于临时服务器或测试场景。

3. backlog 的实际限制

Linux 调整

  • 最终队列长度取 min(backlog, somaxconn)

  • 可通过 /proc/sys/net/core/somaxconn 修改系统上限:

    echo 1024 > /proc/sys/net/core/somaxconn

其他系统:macOS/BSD 的 backlog 可能限制为 5(需通过 sysctl 调整)。

4. 监听套接字 vs 连接套接字

  • 监听套接字sockfd):仅用于接受新连接(accept())。

  • 连接套接字accept() 返回):用于与客户端通信。

6、常见问题

Q1:listen() 和 accept() 的关系?

  • listen():将套接字设为被动监听,初始化连接队列。

  • accept():从已完成队列中取出一个连接,返回新的套接字用于通信。

Q2:如何避免连接队列溢出?

  • 增大 backlog(如 SOMAXCONN)。

  • 调整 net.ipv4.tcp_max_syn_backlog(未完成队列上限)。

  • 优化 accept() 速度,避免长时间阻塞。

Q3:listen() 后能否继续调用 bind()

  • 不能bind() 必须在 listen() 前调用。

Q4:UDP 需要 listen() 吗?

  • 不需要listen() 仅用于面向连接的套接字(如 TCP)。UDP 是无连接的,直接使用 recvfrom()/sendto()

7、示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in server_addr;// 1. 创建 TCP 套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 绑定地址和端口memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(sockfd);exit(EXIT_FAILURE);}// 3. 开始监听if (listen(sockfd, SOMAXCONN) == -1) {perror("listen failed");close(sockfd);exit(EXIT_FAILURE);}printf("Server listening on port 8080...\n");// 4. 接受连接(此处省略 accept() 循环)close(sockfd);return 0;
}

8、总结

  • listen() 的作用是将套接字标记为 被动监听,并初始化连接队列。

  • backlog 参数控制等待队列的长度,影响服务器并发处理能力。

  • 典型流程:socket() → bind() → listen() → accept()

  • 监听套接字仅用于接受连接,实际通信需通过 accept() 返回的新套接字。

通过合理配置 listen(),可以优化服务器的连接处理能力,避免队列溢出导致的连接丢失问题。


四、接收连接请求(TCP服务器)

1、函数原型

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) 是 Unix/Linux 网络编程中用于 从监听套接字的已完成连接队列中提取一个客户端连接 的系统调用函数,主要用于 TCP 服务器。它是服务器端处理客户端连接的关键步骤,通常在 listen() 之后调用。以下是对该函数的详细讲解:

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

2、参数说明

1. sockfd(监听套接字描述符)

作用:由 socket() 创建、bind() 绑定并调用 listen() 的 监听套接字

特点

  • 该套接字处于被动监听状态(LISTEN)。

  • 仅用于接受新连接,不用于实际数据通信。

示例

int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(listen_sock, SOMAXCONN);
int client_sock = accept(listen_sock, &client_addr, &addrlen);  // 提取连接

2. addr(客户端地址结构体指针)

作用:用于 返回客户端的地址信息(如 IP 和端口)。

类型:通常是 struct sockaddr_in(IPv4)或 struct sockaddr_in6(IPv6),但需强制转换为 struct sockaddr*

参数传递

  • 调用前:addr 指向一个足够大的缓冲区(如 struct sockaddr_storage)。

  • 调用后:内核填充客户端地址信息。

  • 若不需要地址信息,可设为 NULL

示例

struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
int client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &addrlen);

3. addrlen(地址长度指针)

作用

  • 输入:告诉内核 addr 缓冲区的长度。

  • 输出:返回实际写入的地址长度。

要求

  • 调用前必须初始化(如 socklen_t addrlen = sizeof(struct sockaddr_in))。

  • 若 addr 为 NULLaddrlen 也应为 NULL

示例

socklen_t addrlen = sizeof(client_addr);  // 初始化
accept(listen_sock, (struct sockaddr*)&client_addr, &addrlen);
printf("Client port: %d\n", ntohs(client_addr.sin_port));  // 打印客户端端口

3、返回值

  • 成功:返回一个新的 已连接套接字描述符client_sock),用于与客户端通信。

  • 失败:返回 -1,并设置 errno(如 EWOULDBLOCK 非阻塞模式下无连接,ECONNABORTED 连接中止)。

4、工作流程

  1. 监听套接字sockfd)通过 listen() 进入被动模式。

  2. 客户端连接:客户端调用 connect(),触发三次握手。

  3. 连接完成:握手完成后,连接进入服务器的 已完成队列ESTABLISHED 状态)。

  4. 提取连接accept() 从队列中取出一个连接,返回一个新的套接字(client_sock)用于通信。

  5. 数据通信:通过 client_sock 使用 read()/write() 或 send()/recv() 交换数据。

// 典型 TCP 服务器 accept 流程
int listen_sock = socket(...);
bind(listen_sock, ...);
listen(listen_sock, SOMAXCONN);while (1) {struct sockaddr_in client_addr;socklen_t addrlen = sizeof(client_addr);int client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &addrlen);if (client_sock == -1) {perror("accept failed");continue;}printf("New client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 处理客户端请求(如读写数据)char buf[1024];read(client_sock, buf, sizeof(buf));write(client_sock, "Hello", 5);close(client_sock);  // 关闭连接套接字
}
close(listen_sock);  // 关闭监听套接字

5、关键注意事项

1. 监听套接字 vs 连接套接字

监听套接字sockfd):

  • 用于接受新连接(accept())。

  • 一个服务器通常只有一个监听套接字。

连接套接字client_sock):

  • 由 accept() 返回,用于与单个客户端通信。

  • 每个客户端连接对应一个独立的套接字。

2. 阻塞与非阻塞模式

默认阻塞模式accept() 会阻塞,直到队列中有已完成连接。

非阻塞模式

  • 通过 fcntl(sockfd, F_SETFL, O_NONBLOCK) 设置。

  • 若队列为空,accept() 立即返回 -1errno 为 EWOULDBLOCK 或 EAGAIN

  • 通常与 select()/poll()/epoll() 配合使用。

3. 多线程/多进程处理

每个 accept() 返回的 client_sock 可独立处理:

  • 多进程fork() 后子进程处理连接,父进程继续监听。

  • 多线程:创建新线程处理 client_sock

  • I/O 多路复用:用 epoll 监听多个 client_sock

4. 地址结构体的兼容性

addr 参数是 struct sockaddr* 类型,但实际使用时需转换为具体类型(如 sockaddr_in):

struct sockaddr_in client_addr;
accept(sockfd, (struct sockaddr*)&client_addr, &addrlen);

使用 sockaddr_storage 可兼容 IPv4 和 IPv6:

struct sockaddr_storage client_addr;
accept(sockfd, (struct sockaddr*)&client_addr, &addrlen);

5. 错误处理

常见错误:

  • EWOULDBLOCK/EAGAIN:非阻塞模式下无连接。

  • ECONNABORTED:连接在队列中时被中止(如客户端超时)。

  • EINTR:被信号中断,可重启调用。

6、常见问题

Q1:accept() 返回的新套接字与监听套接字的关系?

  • 完全独立:新套接字仅用于当前客户端通信,监听套接字继续监听其他连接。

Q2:如果队列中没有连接,accept() 会怎样?

  • 阻塞模式:一直等待,直到有连接。

  • 非阻塞模式:立即返回 -1errno 为 EWOULDBLOCK

Q3:如何获取客户端的 IP 和端口?

  • 通过 addr 和 addrlen 参数返回客户端地址信息:

    struct sockaddr_in *addr_in = (struct sockaddr_in*)addr;
    printf("Client IP: %s, Port: %d\n", inet_ntoa(addr_in->sin_addr), ntohs(addr_in->sin_port));

Q4:accept() 和 connect() 的关系?

  • accept() 是服务器端提取连接,connect() 是客户端发起连接。两者共同完成 TCP 三次握手。

Q5:能否多次调用 accept() 处理多个客户端?

  • 可以!每次调用 accept() 从队列中取出一个连接,直到队列为空。

7、示例代码(完整服务器)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int listen_sock, client_sock;struct sockaddr_in server_addr, client_addr;socklen_t addrlen = sizeof(client_addr);// 1. 创建监听套接字listen_sock = socket(AF_INET, SOCK_STREAM, 0);if (listen_sock == -1) {perror("socket failed");exit(EXIT_FAILURE);}// 2. 绑定地址和端口memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");close(listen_sock);exit(EXIT_FAILURE);}// 3. 开始监听if (listen(listen_sock, SOMAXCONN) == -1) {perror("listen failed");close(listen_sock);exit(EXIT_FAILURE);}printf("Server listening on port 8080...\n");// 4. 循环接受客户端连接while (1) {client_sock = accept(listen_sock, (struct sockaddr*)&client_addr, &addrlen);if (client_sock == -1) {perror("accept failed");continue;}// 打印客户端信息char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);printf("New client: %s:%d\n", client_ip, ntohs(client_addr.sin_port));// 5. 处理客户端请求(示例:回显服务)char buf[1024];ssize_t bytes_read;while ((bytes_read = read(client_sock, buf, sizeof(buf))) > 0) {write(client_sock, buf, bytes_read);  // 回显数据}close(client_sock);  // 关闭客户端连接printf("Client disconnected.\n");}close(listen_sock);  // 关闭监听套接字(实际不会执行到这里)return 0;
}

8、总结

  • accept() 的作用是从监听套接字的已完成队列中提取一个客户端连接,返回新的套接字用于通信。

  • 参数 addr 和 addrlen 用于获取客户端地址信息。

  • 监听套接字和连接套接字分离,支持多客户端并发处理。

  • 阻塞/非阻塞模式、错误处理和多线程/I/O 多路复用是实际开发中的关键点。

通过合理使用 accept(),可以构建高效的 TCP 服务器,处理多个客户端连接。


五、建立连接(TCP客户端)

1、函数原型

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 是 Unix/Linux 网络编程中用于 主动发起 TCP 连接 的系统调用函数,主要用于 TCP 客户端。它尝试与指定的服务器地址建立连接,完成 TCP 三次握手。以下是详细讲解:

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2、参数说明

1. sockfd(套接字描述符)

作用:由 socket() 创建的 主动套接字,用于发起连接。

特点

  • 必须是 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP,但 UDP 通常不调用 connect())。

  • 调用 connect() 后,套接字变为 已连接状态(TCP 可直接用 send()/recv())。

示例

int sockfd = socket(AF_INET, SOCK_STREAM, 0);  // 创建 TCP 套接字

2. addr(服务器地址结构体指针)

作用:指定要连接的 服务器地址和端口

类型:通常是 struct sockaddr_in(IPv4)或 struct sockaddr_in6(IPv6),但需强制转换为 const struct sockaddr*

关键字段(以 sockaddr_in 为例):

  • sin_family:地址族(如 AF_INET)。

  • sin_port:服务器端口(需用 htons() 转换)。

  • sin_addr:服务器 IP 地址(如 inet_addr("192.168.1.1") 或 INADDR_ANY)。

示例

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);  // 服务器端口 8080
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");  // 服务器 IP

3. addrlen(地址结构体长度)

作用:指定 addr 指向的结构体的长度。

要求

  • 必须正确设置(如 sizeof(struct sockaddr_in))。

  • 若 addr 为 NULLaddrlen 也应为 0(但无实际意义)。

示例

socklen_t addrlen = sizeof(server_addr);
connect(sockfd, (struct sockaddr*)&server_addr, addrlen);

3、返回值

  • 成功:返回 0,表示连接建立成功。

  • 失败:返回 -1,并设置 errno(如 ECONNREFUSED 连接被拒绝,ETIMEDOUT 超时)。

4、工作流程

  1. 创建套接字:客户端调用 socket() 创建套接字。

  2. 设置服务器地址:填充 sockaddr_in 结构体(IP + 端口)。

  3. 发起连接:调用 connect(),触发 TCP 三次握手:

    • 客户端发送 SYN 包到服务器。

    • 服务器响应 SYN-ACK

    • 客户端回复 ACK,连接建立。

  4. 连接结果

    • 成功:套接字变为 已连接状态,可直接通信。

    • 失败:返回错误,需处理(如重试或退出)。

// 典型 TCP 客户端 connect 流程
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {perror("socket failed");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);
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("connect failed");close(sockfd);exit(EXIT_FAILURE);
}printf("Connected to server!\n");
// 现在可以通过 sockfd 发送/接收数据
write(sockfd, "Hello", 5);
close(sockfd);

5、关键注意事项

1. 阻塞与非阻塞模式

默认阻塞模式

  • connect() 会阻塞,直到连接成功或失败。

  • 若服务器不可达,可能长时间阻塞(受系统超时设置影响)。

非阻塞模式

  • 通过 fcntl(sockfd, F_SETFL, O_NONBLOCK) 设置。

  • 调用后立即返回 -1errno 为 EINPROGRESS,表示连接正在进行。

  • 需用 select()/poll()/epoll() 监听套接字可写事件,再调用 getsockopt() 检查连接状态。

2. 错误处理

常见错误:

  • ECONNREFUSED:服务器未监听指定端口。

  • ETIMEDOUT:连接超时(如服务器无响应)。

  • ENETUNREACH:网络不可达(如路由错误)。

  • EINPROGRESS(非阻塞模式下连接进行中)。

示例

if (connect(sockfd, (struct sockaddr*)&server_addr, addrlen) == -1) {if (errno == ECONNREFUSED) {printf("Server refused connection.\n");} else if (errno == ETIMEDOUT) {printf("Connection timed out.\n");}close(sockfd);exit(EXIT_FAILURE);
}

3. 地址结构体的兼容性

addr 参数是 const struct sockaddr* 类型,但实际使用时需转换为具体类型(如 sockaddr_in):

struct sockaddr_in server_addr;
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

使用 sockaddr_storage 可兼容 IPv4 和 IPv6:

struct sockaddr_storage server_addr;
// 填充 server_addr...
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));

4. 连接后的套接字行为

TCP 套接字

  • 连接建立后,可直接用 send()/recv() 或 write()/read() 通信。

  • 无需每次通信都指定服务器地址(与 UDP 不同)。

UDP 套接字:通常不调用 connect(),但调用后可通过 send()/recv() 简化操作(仅接收指定地址的数据)。

5. 多次调用 connect()

未连接的套接字:首次调用 connect() 发起连接。

已连接的套接字

  • 再次调用 connect() 会返回 EISCONN 错误(已连接)。

  • 若需重新连接,需先 close() 套接字或调用 connect() 到新地址(部分系统支持)。

6、常见问题

Q1:connect() 失败后能否重试?

可以!但需注意:

  • 立即重试可能失败(如服务器未释放 TIME_WAIT 状态)。

  • 建议延迟后重试(如指数退避算法)。

Q2:如何设置连接超时?

  • 阻塞模式:使用 alarm() 或 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO/SO_SNDTIMEO) 设置超时。

  • 非阻塞模式:结合 select() 实现超时控制。

Q3:connect() 和 bind() 的关系?

  • 通常不需要 bind():客户端通常由内核自动分配临时端口。

  • 需要 bind() 的情况

    • 必须使用特定本地端口(如 FTP 主动模式)。

    • 多网卡环境下需指定源 IP。

Q4:如何获取连接后的本地地址和端口?

  • 调用 getsockname()

    struct sockaddr_in local_addr;
    socklen_t local_len = sizeof(local_addr);
    getsockname(sockfd, (struct sockaddr*)&local_addr, &local_len);
    printf("Local port: %d\n", ntohs(local_addr.sin_port));

Q5:connect() 对 UDP 的作用?

  • UDP 是无连接的,但调用 connect() 后:

    • 只能向该地址发送数据(send() 无需指定目标地址)。

    • 仅接收来自该地址的数据(过滤其他源)。

7、示例代码(完整客户端)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>int main() {int sockfd;struct sockaddr_in server_addr;// 1. 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket failed");exit(EXIT_FAILURE);}// 2. 设置服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);if (inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr) <= 0) {perror("invalid address");close(sockfd);exit(EXIT_FAILURE);}// 3. 发起连接if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("connect failed");close(sockfd);exit(EXIT_FAILURE);}printf("Connected to server!\n");// 4. 发送数据const char *msg = "Hello from client!";write(sockfd, msg, strlen(msg));// 5. 接收响应(可选)char buf[1024];ssize_t bytes_read = read(sockfd, buf, sizeof(buf));if (bytes_read > 0) {buf[bytes_read] = '\0';printf("Server reply: %s\n", buf);}close(sockfd);return 0;
}

8、总结

  • connect() 是 TCP 客户端发起连接的核心函数,完成三次握手。

  • 参数 addr 和 addrlen 用于指定服务器地址。

  • 阻塞/非阻塞模式、错误处理和重试机制是实际开发中的关键点。

  • 连接成功后,套接字可直接用于数据通信(TCP)或简化 UDP 操作。

通过合理使用 connect(),可以构建可靠的 TCP 客户端程序。


六、完整TCP服务器和客户端示例

TCP服务器

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <cstring>#define PORT 8080
#define BUFFER_SIZE 1024int main() {// 创建套接字int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");return -1;}// 设置服务器地址struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);// 绑定套接字if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(server_fd);return -1;}// 监听if (listen(server_fd, 5) == -1) {perror("listen");close(server_fd);return -1;}std::cout << "服务器正在监听端口 " << PORT << "..." << std::endl;// 接受连接struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");close(server_fd);return -1;}// 打印客户端信息char client_ip[INET_ADDRSTRLEN];inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);std::cout << "客户端已连接: " << client_ip << ":" << ntohs(client_addr.sin_port) << std::endl;// 接收和发送数据char buffer[BUFFER_SIZE];ssize_t bytes_received;while ((bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) {std::cout << "收到消息: " << std::string(buffer, bytes_received) << std::endl;// 回显消息if (send(client_fd, buffer, bytes_received, 0) == -1) {perror("send");break;}}if (bytes_received == -1) {perror("recv");}close(client_fd);close(server_fd);return 0;
}

TCP客户端

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
#include <string>#define PORT 8080
#define BUFFER_SIZE 1024int main() {// 创建套接字int client_fd = socket(AF_INET, SOCK_STREAM, 0);if (client_fd == -1) {perror("socket");return -1;}// 设置服务器地址struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {perror("inet_pton");close(client_fd);return -1;}// 连接到服务器if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {perror("connect");close(client_fd);return -1;}std::cout << "已连接到服务器 127.0.0.1:" << PORT << std::endl;std::cout << "输入消息(输入'exit'退出):" << std::endl;// 发送和接收数据std::string message;char buffer[BUFFER_SIZE];while (true) {std::getline(std::cin, message);if (message == "exit") {break;}// 发送消息if (send(client_fd, message.c_str(), message.size(), 0) == -1) {perror("send");break;}// 接收回显ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0);if (bytes_received <= 0) {if (bytes_received == -1) {perror("recv");}break;}std::cout << "服务器回显: " << std::string(buffer, bytes_received) << std::endl;}close(client_fd);return 0;
}

七、总结

  • socket(): 创建套接字,指定协议族和类型

  • bind(): 将套接字绑定到特定地址和端口(服务器端)

  • listen(): 开始监听连接请求(TCP服务器)

  • accept(): 接受客户端连接请求(TCP服务器)

  • connect(): 主动连接到服务器(TCP客户端)

对于UDP协议,不需要建立连接,因此没有listen()和accept()步骤,而是直接使用sendto()和recvfrom()进行数据收发。

这些API构成了网络编程的基础,理解它们的工作原理和相互关系对于开发网络应用程序至关重要。

http://www.dtcms.com/a/594167.html

相关文章:

  • 网站关键词排名怎么提升app开发外包要多少钱
  • 使用 Node.js 开发 Telegram Bot 完整指南
  • 招聘网站代理做网站提供服务器吗
  • AI宠物的情感交互设计与市场反响
  • 【C/C++】C++11 类的 默认构造函数 “= default” 用法
  • 自己建的网站可以用笔记本做服务器吗推广网站建设产品介绍
  • 嵌入式C语言中结构体使用方法与技巧
  • 深度学习(1)—— 基本概念
  • 【Java EE进阶 --- SpringBoot】Spring 核心 --- AOP
  • 4.95基于8086流水灯霓虹彩灯控制器,8086彩灯控制器proteus8.9仿真文件+源码功能四个开关对应四种模式。
  • 网站做百度推广需要什么材料专业的网站制作公司哪家好
  • 在 Ubuntu Desktop Linux 下解压7z文件的完整指南
  • 网站建设工作都包括哪些方面网站论文首页布局技巧
  • 国内做视频的网站网站优化需要做什么
  • 用 LangGraph + MCP Server 打造 SpreadJS 智能助手:让 AI 真正懂你的表格需求
  • 做网站用php还是node外贸网站 备案
  • 行业门户网站源码列举五种网络营销模式
  • 摄影建设网站wordpress插件装多了卡
  • 画世界Pro笔刷大全!含导入教程与多风格笔刷合集
  • 彩笔运维勇闯机器学习--多元线性回归(实战)
  • 免费推广店铺的网站网站默认首页怎么做
  • leetcode1377.T秒后青蛙的位置
  • 基于Yolo的图像识别中的特征融合
  • C语言自定义数据类型详解
  • 社交网站开发 转发建设网站的好处和优点
  • VBUS(Voltage Bus,电压总线) 是什么?
  • 前端做的网站潮阳网站制作
  • 北京哪家公司做网站好网站建设开发报价方案模板
  • 国家商标注册官网查询系统南京seo顾问
  • cpa单页网站怎么做sae wordpress 4.3