常见的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命令测试以上服务