C语言网络编程模型主要包括以下几种:
1、基本TCP Socket编程模型
创建Socket:使用socket()
函数创建一个套接字,指定协议族(如AF_INET表示IPv4)、套接字类型(如SOCK_STREAM表示TCP流式套接字)和协议(通常为0,表示使用默认协议)。
int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); }
绑定地址:使用bind()
函数将套接字与本地地址(IP地址和端口号)绑定,需要先定义一个struct sockaddr_in
结构体来存储地址信息,并设置其成员变量,然后将其指针传递给bind()
函数。
struct sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); if (bind(sockfd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); }
监听连接:对于服务器端,使用listen()
函数使套接字进入监听状态,等待客户端的连接请求,需要指定监听套接字的文件描述符和等待连接的队列长度。
if (listen(sockfd, 3) < 0) { perror("listen"); exit(EXIT_FAILURE); }
接受连接:服务器端调用accept()
函数从监听套接字的连接队列中接受一个连接请求,返回一个新的套接字描述符用于与客户端通信。
int new_socket; struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); new_socket = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len); if (new_socket < 0) { perror("accept"); exit(EXIT_FAILURE); }
发送数据:使用send()
或write()
函数向已连接的客户端发送数据,需要指定套接字描述符、要发送的数据缓冲区及其长度等参数。
char *message = "Hello from server"; send(new_socket, message, strlen(message), 0);
接收数据:使用recv()
或read()
函数接收客户端发送的数据,同样需要指定套接字描述符、接收数据的缓冲区及其长度等参数。
char buffer[1024] = {0}; int valread = read(new_socket, buffer, 1024); printf("%s ", buffer);
关闭套接字:在通信结束后,使用close()
函数关闭套接字,释放资源。
close(new_socket); close(sockfd);
2、基于多进程的客户端 服务器模型
服务器端:主进程负责监听客户端的连接请求,每当有新的客户端连接时,通过fork()
系统调用创建一个子进程来处理该客户端的请求,子进程继承父进程的监听套接字,并在处理完客户端请求后退出,这样可以实现多个客户端同时与服务器进行通信,提高服务器的并发处理能力。
int main() { int server_fd, new_socket; struct sockaddr_in address; int opt = 1; int addrlen = sizeof(address); char buffer[1024] = {0}; // 创建监听套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置套接字选项 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); // 绑定套接字 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 开始监听 if (listen(server_fd, 3) < 0) { perror("listen"); exit(EXIT_FAILURE); } printf("Server is listening on port %d... ", PORT); while (1) { if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept"); exit(EXIT_FAILURE); } // 创建子进程处理客户端请求 pid_t child_pid = fork(); if (child_pid == 0) { // 子进程 close(server_fd); // 关闭监听套接字 // 处理客户端请求... char *response = "Hello from server"; send(new_socket, response, strlen(response), 0); close(new_socket); // 关闭与客户端的连接套接字 exit(0); // 子进程退出 } else if (child_pid < 0) { // fork失败 perror("fork"); exit(EXIT_FAILURE); } else { // 父进程 close(new_socket); // 父进程关闭与客户端的连接套接字 } } }
客户端:与基本TCP Socket编程模型中的客户端类似,创建套接字、连接到服务器、发送和接收数据等,只是在与服务器建立连接后,可以与对应的服务器子进程进行通信。
3、基于多线程的客户端 服务器模型
服务器端:与基于多进程的模型类似,主线程负责监听客户端的连接请求,当有新连接时,创建一个新线程来处理该客户端的请求,可以使用pthread_create()
函数创建线程,并将客户端套接字描述符传递给线程函数作为参数。
void *thread_callback(void *args) { int client_socket = *((int *)args); // 处理客户端请求... char *response = "Hello from server"; send(client_socket, response, strlen(response), 0); close(client_socket); // 关闭与客户端的连接套接字 return NULL; } int main() { int server_fd, new_socket; struct sockaddr_in address; int opt = 1; int addrlen = sizeof(address); char buffer[1024] = {0}; // 创建监听套接字... // 绑定、监听... while (1) { if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept"); exit(EXIT_FAILURE); } pthread_t thread_id; pthread_create(&thread_id, NULL, thread_callback, (void *)&new_socket); } }
客户端:同样与基本TCP Socket编程模型中的客户端类似,这种模型相对于多进程模型,线程的创建和销毁开销相对较小,但在高并发情况下,可能会受到操作系统对线程数量的限制。
4、基于IO多路复用的客户端 服务器模型
select模型:使用select()
函数监视多个文件描述符的状态变化,判断是否有文件描述符可读、可写或异常,服务器端将监听套接字和所有已连接的客户端套接字都添加到select()
函数的监视集合中,当有客户端套接字可读时,表示有客户端发送了数据,服务器可以读取并进行相应的处理。
#define MAX_CLIENTS 30 int main() { int server_fd, new_socket, client_socket[MAX_CLIENTS], max_sd, activity, i, valread, sd; int opt = 1; struct sockaddr_in address; int addrlen = sizeof(address); char buffer[1024]; // 创建监听套接字... // 绑定、监听... // 初始化客户端套接字数组 for (i = 0; i < MAX_CLIENTS; i++) { client_socket[i] = 0; } while (1) { // 清空集合 fd_set readfds; FD_ZERO(&readfds); // 添加监听套接字到集合 FD_SET(server_fd, &readfds); max_sd = server_fd; // 添加客户端套接字到集合 for (i = 0; i < MAX_CLIENTS; i++) { sd = client_socket[i]; if (sd > 0) { FD_SET(sd, &readfds); if (sd > max_sd) { max_sd = sd; } } } // 等待活动发生 activity = select(max_sd + 1, &readfds, NULL, NULL, NULL); if ((activity < 0) && (errno != EINTR)) { perror("select error"); } // 如果监听套接字可读,表示有新的连接请求 if (FD_ISSET(server_fd, &readfds)) { if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept"); } else { for (i = 0; i < MAX_CLIENTS; i++) { if (client_socket[i] == 0) { client_socket[i] = new_socket; break; } } } } // 检查客户端套接字的活动情况 for (i = 0; i < MAX_CLIENTS; i++) { sd = client_socket[i]; if (FD_ISSET(sd, &readfds)) { // 读取客户端数据... valread = read(sd, buffer, 1024); if (valread == 0) { close(sd); client_socket[i] = 0; } else { // 处理客户端数据... } } } }
poll模型:与select()
函数类似,但poll()
函数没有文件描述符数量的限制,并且可以为每个文件描述符设置不同的事件和超时时间,它使用一个struct pollfd
结构体数组来存储要监视的文件描述符及其事件信息。
#define MAX_CLIENTS 30 int main() { int server_fd, new_socket, client_socket[MAX_CLIENTS], i, valread, sd; int opt = 1; struct sockaddr_in address; int addrlen = sizeof(address); char buffer[1024]; struct pollfd pollfds[MAX_CLIENTS]; // 创建监听套接字... // 绑定、监听... // 初始化客户端套接字数组和pollfds数组 for (i = 0; i < MAX_CLIENTS; i++) { client_socket[i] = 0; pollfds[i].fd = -1; } while (1) { // 清空pollfds数组中的revents字段 for (i = 0; i < MAX_CLIENTS; i++) { pollfds[i].revents = 0; } // 设置监听套接字的事件为POLLIN(可读) // 设置客户端套接字的事件为POLLIN(可读)和POLLERR(错误) for (i = 0; i < MAX_CLIENTS; i++) { if (client_socket[i] > 0) { pollfds[i].fd = client_socket[i]; pollfds[i].events = POLLIN | POLLERR; } } } // 等待事件发生 int activity = poll(pollfds, MAX_CLIENTS, -1); if (activity < 0) { perror("poll error"); return -1; } for (i = 0; i < MAX_CLIENTS; i++) { if (pollfds[i].revents & POLLIN) { sd = pollfds[i].fd; if ((sd == server_fd) || (client_socket[i] == -1)) { // 接受新的连接请求... } else { // 读取客户端数据... } } else if (pollfds[i].revents & POLLERR) { close(pollfds[i].fd); client_socket[i] = -1; } } }
epoll模型:是Linux下高效的IO多路复用模型,它使用epoll_create()
函数创建一个epoll实例,然后使用epoll_ctl()
函数将文件描述符添加到epoll实例的监视列表中,并设置相应的事件类型(如EPOLLIN表示可读),最后使用epoll_wait()
函数等待事件发生,它会返回就绪的文件描述符列表。
#define MAX_EVENTS 1000 #include <sys/epoll.h> int main() { int server_fd, epollfd, new_socket, client_socket[MAX_EVENTS], i, valread, sd; int opt = 1; struct sockaddr_in address; int addrlen = sizeof(address); char buffer[1024]; struct epoll_event ev, events[MAX_EVENTS]; // 创建监听套接字... // 绑定、监听... // 创建epoll实例 epollfd = epoll_create1(0); if (epollfd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } // 将监听套接字添加到epoll实例的监视列表中 ev.events = EPOLLIN; ev.data.fd = server_fd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_fd, &ev) == -1) { perror("epoll_ctl: server_fd"); exit(EXIT_FAILURE); } while (1) { // 等待事件发生 int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (i = 0; i < nfds; i++) { if (events[i].data.fd == server_fd) { // 接受新的连接请求... } else { // 读取客户端数据... } } } }