常见的socket编程的api有
- socket() 创建socket fd标识符
- setsocketopt() 设置socket工作参数
- bind() 绑定socket到本地的ip + port
- listen() 启动服务 监听socket fd
- accept() 接受一个socket连接,阻塞等待socket连接
socket函数
创建一个socket的内核对象 返回的是一个fd(file descriptor),关联于当前进程的所有fd的一个自增的id号,映射到内核是一个socket的对象地址
创建一个IPv4的TCP Socket方法如下
// PF_INET 使用IPv4协议 // SOCK_STREAM 使用顺序、可靠的双向数据流 // IPPROTO_TCP 使用TCP协议 int server_socket_fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
setsocketopt函数 设置socket工作参数
该函数可以设置任意socket的所有可配置参数详细可查看手册
设置socket address重用,如果不设置的话重启的时候会报错
错误代码 EADDRINUSE
提示信息 Address already in use
为什么会这样呢? 是因为TCP的连接断开后 TCP的资源并没有立即释放,而是进入了TIME_WAIT状态,该状态是为了保证TCP的可靠关闭,该状态会持续2分钟
如果服务器异常终止,TCP并不会直接被回收,因为TCP是一个可靠的服务,所以内核会自动维护TCP进入TIME_WAIT状态,向当前连接的客户端继续应答TCP关闭的ACK消息
int isReuse = 1; setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, (const char *) &isReuse, sizeof isReuse);
bind函数
绑定socket到一个特定的地址+端口和协议上,如果不绑定的话 调用listen、connect函数内核会自动分配一个随机的地址和端口号,服务端需要绑定固定的端口所以listen前一定需要绑定一个固定的端口协议上
如下代码是绑定socket到 本地的0.0.0.0:80端口上
// 确定本地服务地址 + 端口号 struct sockaddr_in server_address = {0}; server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = htonl(INADDR_ANY); server_address.sin_port = htons(8082); bind(server_socket_fd, (const struct sockaddr *) &server_address, sizeof(server_address));
listen函数
启动对socket指定的ip+port的监听,可以接受远端的客户端连接
MAX_CONNECTIONS 是自己定义的服务可以接受的最大连接数
errno = listen(server_socket_fd, MAX_CONNECTIONS);
accept函数
该函数是阻塞式的,调用该函数如果没有客户端的话,则进程会被阻塞,该函数不会返回,直到有一个客户端连接才会返回对应客户端的socket fd
构建一个IPv4的TCP地址结构,然后传递给accept函数,该函数会一直等待客户端连接才返回
struct sockaddr_in client_address = {0}; int client_socket_length = sizeof(struct sockaddr_in); int client_fd = accept(master_socket_fd, &client_address, &client_socket_length);
异步Socket编程如何实现?
linux提供了一个select函数通过轮询一批指定的fd状态来实现异步的socket操作相关的api如下
- fd_set fd集合数据类型 只能通过下面的FD_XX()宏操作
- FD_ZERO(&fdset) 清空指定的fd合集
- FD_SET(fd, &fdset) 向指定合集添加fd
- FD_ISSET(fd, &fdset) 检查指定fd 是否存在于fd合集中
- FD_COPY(&src, &dist) 复制src合集到dist中
- FD_CLR(fd, &fdset) 从指定合集中删除指定fd
- select 检查fd合集状态
select 函数
该函数用于检查给定的fd合集是否可操作,而且可以设置超时时间
select函数会修改传入的三个fd合集内容,函数执行成功后,会返回一个可执行操作的fd数量,传入的集合中只会保留下需要操作的fd
所以select函数后就可以通过FD_ISSET宏来检查对应的fd是否可以操作了
如果返回值等于0则没有fd可操作,如果返回值小于0则select函数出错
函数原型如下
int select( int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout );
nfds 检查次数
指的是 readfds、writefds、errorfds、三个合集中所有的fd的最大值 + 1,表示select函数对传入的每个合集检查的次数 – 1
假设readfds中设置了两个fd,10和20,writefds中设置了1个fd 30,这时候nfds传入的值应该是30 + 1个
readfds、writefds、errorfds 检查的fd合集
需要检查的三种类型的fd合集,如果某个没有直接传入NULL即可,该集合会在函数执行完成后被修改为只有需要操作的fd集合
timeout 超时时间
如果该参数传入NULL的话 select函数会阻塞
如果该参数传入一个空的 struct timeval 结构 则select会检查完所有的fd集合并返回变动的fd数量
如果传入的结构限制了执行时间的话,超时select也会返回,但是可能会有些集合没有遍历完成
demo如下
#include <stdio.h> #include <libc.h> #define printf(...) printf(__VA_ARGS__); printf("\n"); #define MAX_CONNECTIONS 10 struct session { // socket 读写端 int socket_fd; // 会话链表 struct session *next; }; struct session *root_session = NULL; struct session *disconnect(struct session *s) { struct session *prev = NULL, *ps = root_session; while (ps != NULL) { if (ps == s) { if (prev == NULL) { root_session = ps->next; } else { prev->next = ps->next; } close(ps->socket_fd); free(ps); break; } prev = ps; ps = ps->next; } return root_session; } struct session *new_session() { struct session *s = malloc(sizeof(struct session)); memset(s, 0, sizeof(struct session)); return s; } int main() { // 创建socket对象 int server_socket_fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 确定本地服务地址 + 端口号 struct sockaddr_in server_address = {0}; server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = htonl(INADDR_ANY); server_address.sin_port = htons(8082); int isReuse = 1; int errno = setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, (const char *) &isReuse, sizeof isReuse); if (errno < 0) { printf("SO_REUSEADDR socket error = %d", errno); return errno; } errno = bind(server_socket_fd, (const struct sockaddr *) &server_address, sizeof(server_address)); if (errno < 0) { printf("bind socket error = %d", errno); return errno; } errno = listen(server_socket_fd, MAX_CONNECTIONS); if (errno < 0) { printf("listen socket error = %d", errno); return errno; } printf( "socket server listened to %d", ntohs(server_address.sin_port) ); fflush(stdout); fd_set read_fds; fd_set write_fds; struct timeval timeout = {0}; // 初始化一个空的超时结构 int max_fd = server_socket_fd; // maxfd等于服务器的fd // 死循环操作等待socket连接 while (1) { FD_ZERO(&read_fds); FD_ZERO(&write_fds); FD_SET(server_socket_fd, &read_fds); max_fd = server_socket_fd; struct session *next, *session = root_session; // 遍历链表把socket fd全部放入fdset中 while (session != NULL) { if (session->socket_fd == -1) { next = session->next; disconnect(session); session = next; continue; } else { FD_SET(session->socket_fd, &read_fds); FD_SET(session->socket_fd, &write_fds); max_fd = session->socket_fd; } session = session->next; } // 检查 读取 写入 错误 三种类型的文件是否可以操作 errno = select(max_fd + 1, &read_fds, &write_fds, NULL, &timeout); if (errno == 0) { // 没有变化 跳过此次循环 continue; } if (errno < 0) { // socket 错误退出进程 printf("socket select error = %d", -1); return -1; } // 检查client socket 读写 session = root_session; while (session != NULL) { // socket 可读 if (FD_ISSET(session->socket_fd, &read_fds)) { char read_buf[1024] = {0}; int read_size = read(session->socket_fd, read_buf, sizeof(read_buf)); if (read_size < 0) { printf("socket client error fd = %d", session->socket_fd); session->socket_fd = -1; continue; } if (read_size == 0) { printf("socket client disconnected fd = %d", session->socket_fd); session->socket_fd = -1; continue; } char real_buf[1024] = {0}; strncpy(real_buf, read_buf, read_size); printf("socket client read = %s", real_buf); if (FD_ISSET(session->socket_fd, &write_fds)) { // DO THE ECHO errno = write(session->socket_fd, real_buf, read_size); if (errno < 0) { printf("socket client write error = %d", errno); } } else { printf("socket client can't write data"); } } session = session->next; } // 有新客户端连接 if (FD_ISSET(server_socket_fd, &read_fds)) { struct sockaddr_in client_address = {0}; int client_socket_length = sizeof(struct sockaddr_in); int client_fd = accept(server_socket_fd, &client_address, &client_socket_length); if (client_fd < 0) { printf("socket client accept error = %d", client_fd); continue; } struct session *new_client = new_session(); new_client->socket_fd = client_fd; printf( "client socket address = %s port = %d", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port) ); // 添加新的socket到链表中 if (root_session == NULL) { root_session = new_client; } else { session = root_session; while (session->next != NULL) { session = session->next; } session->next = new_client; } } // 强制刷新printf数据到控制台上 fflush(stdout); } }
可以使用telnet或者nc命令测试以上服务