一、IO模型
1.分类
在UNIX/Linux下主要有4种I/O 模型:
阻塞I/O:最常用、最简单、效率最低
非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
I/O 多路复用:允许同时对多个I/O进行控制
信号驱动I/O:一种异步通信模型------底层驱动专栏中详细讲
2.阻塞IO
以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,进程会被挂起,一直阻塞,直到缓冲区中有内容了,内核会唤醒该进程,读完内容后继续向下执行。
写操作也是会阻塞的,当缓冲区满了,就阻塞了,当缓冲区中有足够的空间接收这次写了就能解除阻塞。一般情况下,对于阻塞的问题,考虑的都是读的阻塞。
示例:以写阻塞为例
//写端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
int fd = open("my_fifo", O_WRONLY);
if(-1 == fd){
perror("open error");
exit(-1);
}
int count = 0;
while(1){
if(-1 == write(fd, "hello world", 11)){
perror("write error");
exit(-1);
}
count++;
printf("count = %d\n", count);
}
close(fd);
return 0;
}
//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(){
int fd = open("my_fifo", O_RDONLY);
char buff[11] = {0};
read(fd, buff, 11);
while(1);//防止管道破裂
close(fd);
return 0;
}
3.非阻塞IO
以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,相当于告诉内核,不要将这个进程挂起,而是立即给我返回一个错误。
一般的带有阻塞属性的函数,默认方式都是阻塞IO。对于recv recvfrom 等函数,是可以通过参数来设置成非阻塞的。如:recv 的 MSG_DONTWAIT,recvfrom 的 MSG_DONTWAIT,waitpid 的 WNOHANG等。但是对于 read 等函数,默认方式就是阻塞,如果想使用read实现非阻塞,需要用到 fcntl() 来修改文件描述符的状态。
fcntl函数说明:
int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置或获取文件描述符的状态
#include <unistd.h>
#include <fcntl.h>
参数:
@fd: 文件描述符
@cmd: 要控制的指令
F_GETFL 获取文件描述符的状态
F_SETFL 设置文件描述符的状态 O_NONBLOCK 非阻塞
@arg: 可变参
具体需不需要取决于第二个参数是什么,
如果第二个参数是 F_GETFL 就不需要
如果第二个参数是 F_SETFL 就需要
返回值: F_GETFL 返回的就是文件描述符的状态
F_SETFL 成功返回0 失败返回-1
示例:使用管道时,注意,写端未打开,读端的open会阻塞,需要设置成非阻塞才能读到。
//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd1 = open("fifo1", O_RDONLY);
if(-1 == fd1){
perror("open error");
exit(-1);
}
int fd2 = open("fifo2", O_RDONLY);
if(-1 == fd2){
perror("open error");
exit(-1);
}
int fd3 = open("fifo3", O_RDONLY);
if(-1 == fd3){
perror("open error");
exit(-1);
}
//将文件描述符 fd1 fd2 fd3 设置成非阻塞
int flag = fcntl(fd1, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd1, F_SETFL, flag);
flag = fcntl(fd2, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd2, F_SETFL, flag);
flag = fcntl(fd3, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd3, F_SETFL, flag);
char buff1[128] = {0};
char buff2[128] = {0};
char buff3[128] = {0};
while(1){
read(fd1, buff1, 128);
printf("buff1 = %s\n", buff1);
memset(buff1, 0, 128);
read(fd2, buff2, 128);
printf("buff2 = %s\n", buff2);
memset(buff2, 0, 128);
read(fd3, buff3, 128);
printf("buff3 = %s\n", buff3);
memset(buff3, 0, 128);
//sleep(1);//为了演示现象用的 防止刷屏
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
//写端(三个写端一样的)
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(){
int fd = open("fifo1", O_WRONLY);
if(-1 == fd){
perror("open error");
exit(-1);
}
char buff[128] = {0};
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';
if(-1 == write(fd, buff, 128)){
perror("write error");
exit(-1);
}
memset(buff, 0, 128);
}
close(fd);
return 0;
}
4.IO多路复用
使用阻塞的方式处理多个阻塞函数,相互之间会有影响,有时不可取。如果使用非阻塞,有需要写一个循环轮询每个函数,十分占用CPU,也不可取。使用多进程、多线程也可以解决这个问题,但是要考虑资源的回收及安全问题,比较麻烦。比较好的一种方式,是使用 IO 多路复用。
IO多路复用的基本思想:
先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
select函数说明:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:IO多路复用
#include <sys/select.h>
参数:
@nfds: 最大的文件描述符+1
@readfds: 要监控的读文件描述符集合 我们一般考虑读
@writefds: 要监控的写文件描述符集合
@exceptfds: 要监控的异常文件描述符集合
@timeout: 超时时间
有值:阻塞的时间,超时后 select会立即返回
0: 非阻塞
NULL:永久阻塞
返回值: 成功返回已经就绪的文件描述符的个数,超时返回0,失败返回-1
注:FD_SETSIZE:select 能监视的最大的文件描述符个数是1024
文件描述符相关函数:
void FD_CLR(int fd, fd_set *set); //在集合中删除一个文件描述符
int FD_ISSET(int fd, fd_set *set); //判断文件描述符是否在集合中
void FD_SET(int fd, fd_set *set); //向集合中添加一个文件描述符
void FD_ZERO(fd_set *set); //将集合清空
示例:
//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
int main(){
int fd1 = open("fifo1", O_RDONLY);
int fd2 = open("fifo2", O_RDONLY);
int fd3 = open("fifo3", O_RDONLY);
int max_fd = 0;//保存最大的文件描述符
//构建要监视的文件描述符集合
fd_set readfds;//保存初始的
fd_set readfds_temp;//给select用的
FD_ZERO(&readfds);//清空
FD_ZERO(&readfds_temp);//清空
//将要监视的文件描述符添加进集合
FD_SET(fd1, &readfds);
max_fd = (max_fd>fd1?max_fd:fd1);
FD_SET(fd2, &readfds);
max_fd = (max_fd>fd2?max_fd:fd2);
FD_SET(fd3, &readfds);
max_fd = (max_fd>fd3?max_fd:fd3);
char buff1[128] = {0};
char buff2[128] = {0};
char buff3[128] = {0};
while(1){
//注意:每次select返回都会将没有准备好的文件描述符在表中擦除
//所以每次要重新将文件描述符添加到集合中
readfds_temp = readfds;
if(-1 == select(max_fd+1, &readfds_temp, NULL, NULL, NULL)){
perror("select error");
exit(-1);
}
if(FD_ISSET(fd1, &readfds_temp)){
read(fd1, buff1, 128);
printf("buff1 = [%s]\n", buff1);
memset(buff1, 0, 128);
}
if(FD_ISSET(fd2, &readfds_temp)){
read(fd2, buff2, 128);
printf("buff2 = [%s]\n", buff2);
memset(buff2, 0, 128);
}
if(FD_ISSET(fd3, &readfds_temp)){
read(fd3, buff3, 128);
printf("buff3 = [%s]\n", buff3);
memset(buff3, 0, 128);
}
}
close(fd1);
close(fd2);
close(fd3);
return 0;
}
//写端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(){
int fd = open("fifo1", O_WRONLY);
if(-1 == fd){
perror("open error");
exit(-1);
}
char buff[128] = {0};
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';
if(-1 == write(fd, buff, 128)){
perror("write error");
exit(-1);
}
memset(buff, 0, 128);
}
close(fd);
return 0;
}
二、服务器模型
1.概念
服务器模型主要有两种:
循环服务器:同一时间只能处理一个客户端的请求。
并发服务器:可以同时处理多个客户端的请求。
TCP服务器本身就是一个循环服务器,原因是他有两个阻塞函数,accept 和 recv 他们之间会相互影响。UDP服务器本身就是一个并发服务器,因为他只有一个阻塞函数,recvfrom
2.循环服务器
在上一篇博客(C语言编程实现TCP/UDP/TFTP网络通信)中详细讲解了,这里就不说了。
示例:(跟下面做对照)
//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
//网络字节序的端口号,可以是 8888 9999 6789 等都可以
server_addr.sin_port = htons(atoi(argv[2]));
//IP地址
//不能随便填,可以填自己主机的IP地址
//如果只是在本地测试,也可以填 127.0.0.1
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
ERRLOG("listen error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
char buff[128] = {0};
int acceptfd = 0;
int bytes = 0;
while(1){
//5.阻塞等待客户端连接
acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
if(-1 == acceptfd){
ERRLOG("accept error");
}
printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
while(1){
//6.与客户端通信
if(0 > (bytes = recv(acceptfd, buff, 128, 0))){
ERRLOG("recv error");
}if(bytes == 0){
printf("客户端 %s:%d 断开了连接\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}else{
if(0 == strcmp(buff, "quit")){
printf("客户端 %s:%d 退出了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}
printf("%s-%d:[%s]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
//组装应答
strcat(buff, "--server");
if(-1 == send(acceptfd, buff, 128, 0)){
ERRLOG("send error");
}
}
}
//7.关闭套接字
close(acceptfd);
}
close(sockfd);
return 0;
}
//客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("connect error");
}
//4.与服务器通信
char buff[128] = {0};
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
if(-1 == send(sockfd, buff, 128, 0)){
ERRLOG("send error");
}
if(0 == strcmp(buff, "quit")){
break;
}
if(-1 == recv(sockfd, buff, 128, 0)){
ERRLOG("recv error");
}
printf("收到回复:[%s]\n", buff);
}
//5.关闭套接字
close(sockfd);
return 0;
}
3.并发服务器
实现TCP并发服务器方式:大多数场景下,我们既要保证可靠,又要保证并发,所以就要研究TCP如何实现并发服务器。
方式1:使用多线程实现TCP并发服务器
方式2:使用多进程实现TCP并发服务器
方式3:使用IO多路复用实现TCP并发服务器(最常用)
4.使用多线程实现TCP并发服务器
主线程专门用来接收客户端的连接请求(也就是专门用来处理 accept)
每当有新的客户端连接成功时,就创建一个子线程,在线程处理函数中专门用来和这个客户端通信。
注:多线程的相关知识在IO接口专栏中的 “c语言中的多线程的实现” 博客详细介绍了。
示例:
//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
typedef struct MSG{
int acceptfd;
struct sockaddr_in client_addr;
}msg_t;
void *deal_recv_send(void *arg){
msg_t msg = *(msg_t *)arg;
printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port));
int bytes = 0;
char buff[128] = {0};
while(1){
//6.与客户端通信
if(0 > (bytes = recv(msg.acceptfd, buff, 128, 0))){
ERRLOG("recv error");
}else if(bytes == 0){
printf("客户端 %s:%d 断开了连接\n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port));
break;
}else{
if(0 == strcmp(buff, "quit")){
printf("客户端 %s:%d 退出了\n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port));
break;
}
printf("%s-%d:[%s]\n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port), buff);
//组装应答
strcat(buff, "--server");
if(-1 == send(msg.acceptfd, buff, 128, 0)){
ERRLOG("send error");
}
}
}
//7.关闭套接字
close(msg.acceptfd);
}
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
ERRLOG("listen error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
int acceptfd = 0;
pthread_t tid = 0;
msg_t client_msg;
while(1){
//5.阻塞等待客户端连接
acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
if(-1 == acceptfd){
ERRLOG("accept error");
}
//将客户端的套接字和客户端的网络信息结构体传给线程处理函数
client_msg.acceptfd = acceptfd;
client_msg.client_addr = client_addr;
//创建线程单独处理和客户端的通信
if(0 != pthread_create(&tid, NULL, deal_recv_send, &client_msg)){
ERRLOG("pthread_create error");
}
//设置线程分离属性
if(0!=pthread_detach(tid)){
ERRLOG("pthread_detach error");
}
}
close(sockfd);
return 0;
}
客户端代码同循环服务器
5.使用多进程实现TCP并发服务器
父进程专门用来接收客户端的连接请求(也就是专门用来处理 accept)
每当有新的客户端连接成功时,就创建一个子进程,在子进程中专门用来和这个客户端通信。
注:多进程的相关知识在IO接口专栏中的 “c语言中的多进程的实现” 博客详细介绍了。
示例:
//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
//自定义的信号处理函数
void deal_child(int x){
wait();//阻塞
//waitpid(-1, NULL, W_NOHONG);//非阻塞
//使用阻塞好一些,如果使用非阻塞,子进程发射出退出信号后,再退出
//有可能导致父进程没有回收到资源,还是会有僵尸进程产生
}
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
ERRLOG("listen error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
int acceptfd = 0;
pthread_t tid = 0;
pid_t pid = 0;
while(1){
//5.阻塞等待客户端连接
acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
if(-1 == acceptfd){
ERRLOG("accept error");
}
//创建子进程 单独处理和该客户端的通信
if(-1 == (pid = fork())){
ERRLOG("fork error");
}else if(pid == 0){
//子进程的逻辑
printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
int bytes = 0;
char buff[128] = {0};
while(1){
//6.与客户端通信
if(0 > (bytes = recv(acceptfd, buff, 128, 0))){
ERRLOG("recv error");
}else if(bytes == 0){
printf("客户端 %s:%d 断开了连接\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}else{
if(0 == strcmp(buff, "quit")){
printf("客户端 %s:%d 退出了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}
printf("%s-%d:[%s]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
//组装应答
strcat(buff, "--server");
if(-1 == send(acceptfd, buff, 128, 0)){
ERRLOG("send error");
}
}
}
//关闭套接字
close(acceptfd);
//子进程退出前 给父进程发射 SIGUSR1 信号
kill(getppid(), SIGUSR1);
exit(0);
}else if(pid >0 ){
//父进程的逻辑
//父进程需要回收子进程的资源,防止僵尸进程
//方式1:wait 但是wait本身也是阻塞,不推荐
//方式2:waitpid 的 W_NOHONG 非阻塞,需要轮询,也不推荐
//方式3:父进程退出了子进程资源就回收了 但是服务器程序一般不会退出
//方式4:使用信号的方式处理比较好:
//子进程退出时,给父进程发一个信号 SIGCHLD 或者使用 SIGUSR1 也行
//父进程就干自己的活(等待客户端连接)
//什么时候收到了子进程退出的信号,然后再去回收子进程的资源
//捕获子进程的退出发射的信号
signal(SIGUSR1, deal_child);
//关闭父进程的 acceptfd
close(acceptfd);
}
}
close(sockfd);
return 0;
}
客户端代码同循环服务器
6.多路IO复用实现TCP并发服务器
将sockfd,和每个客户端的acceptfd 都放到一个表里,传参给select函数
内核帮我们检测哪些文件描述符准备就绪了,select会将准备就绪的文件描述符告诉我们,再根据描述符的不同,分别处理需求即可。
示例:
//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/select.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
ERRLOG("listen error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&server_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
int max_fd = 0;
//构建文件描述符表
fd_set readfds;//是我们自己填充的,相当于备份
fd_set readfds_temp;//是给 select用的 因为每次擦除
FD_ZERO(&readfds);
FD_ZERO(&readfds_temp);
//将sockfd 添加进集合
FD_SET(sockfd, &readfds);
max_fd = max_fd>sockfd?max_fd:sockfd;//更新最大文件描述符
//设置超时时间 5s
struct timeval tm;
memset(&tm, 0, sizeof(tm));
tm.tv_sec = 5;
tm.tv_usec = 0;
int ret = 0;
int acceptfd = 0;
int i = 0;
int bytes = 0;
char buff[128] = {0};
while(1){
//每次重置readfds_temp
readfds_temp = readfds;
//每次重置超时时间
tm.tv_sec = 5;
tm.tv_usec = 0;
if(-1 == (ret = select(max_fd+1, &readfds_temp, NULL, NULL, &tm))){
ERRLOG("select error");
}else if(ret == 0){
printf("select timeout\n");
continue;
}else if(ret > 0){
//判断条件的 ret != 0 是表示:如果n个就绪,只处理n个即可,后面的就不用管了
for(i = 3; i < max_fd+1 & ret != 0; i++){
if(FD_ISSET(i, &readfds_temp)){
if(i == sockfd){
//说明有新的客户端连接
if(-1 == (acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen))){
ERRLOG("accept error");
}
printf("客户端 [%d] 连接到服务器\n", acceptfd);
//连接成功了 将新的客户端的文件描述符加入到表中
FD_SET(acceptfd, &readfds);
//更新max_fd
max_fd = max_fd>acceptfd?max_fd:acceptfd;
}else{
//6.与客户端通信
if(0 > (bytes = recv(i, buff, 128, 0))){
ERRLOG("recv error");
}else if(bytes == 0){
printf("客户端 [%d] 断开了连接\n", i);
//将文件描述符在表中删除
FD_CLR(i, &readfds);
//关闭对应的文件描述符
close(i);
continue;
}else{
if(0 == strcmp(buff, "quit")){
printf("客户端 [%d] 退出了\n", i);
//将文件描述符在表中删除
FD_CLR(i, &readfds);
//关闭对应的文件描述符
close(i);
continue;
}
printf("客户端 [%d] 发来消息:[%s]\n", i, buff);
//组装应答
strcat(buff, "--server");
if(-1 == send(i, buff, 128, 0)){
ERRLOG("send error");
}
}
}
ret--;
}
}
}
}
close(sockfd);
return 0;
}
客户端代码同循环服务器
三、网络超时检测
1.概念
阻塞IO:当程序运行到IO函数时,如果缓冲区中有内容,则程序正常运行,如果没有内容,程序就会阻塞,直到有内容了再继续运行。
非阻塞:当程序运行到IO函数时,如果缓冲区中有内容,则程序正常运行,如果没有内容,程序不会阻塞,而是立刻返回错误。
超时检测:是介于阻塞和非阻塞之间的,可以设定一个时间,在这个时间范围内,如果缓冲区没有内容,就阻塞,如果到了设定的时间,缓冲区中还没有内容,就会变成非阻塞,立刻返回错误。
2.实现超时检测的方式
方式1:select 函数实现超时检测(poll 和 epoll_wait 也可以)
方式2:可以使用 setsockopt 函数设置超时检测
方式3:可以使用alarm信号 实现超时检测
3.使用select实现超时检测
select函数补充:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:最后一个参数,就是要设置的超时时间
#include <sys/select.h>
参数:
@nfds: 最大的文件描述符+1
@readfds: 要监控的读文件描述符集合 我们一般考虑读
@writefds: 要监控的写文件描述符集合
@exceptfds: 要监控的异常文件描述符集合
@timeout: 超时时间
struct timeval:阻塞一定时间
struct timeval {
long tv_sec; /* 秒数 */
long tv_usec; /* 微秒数 */
};
NULL:永久阻塞
0:非阻塞
返回值: 成功返回已经就绪的文件描述符的个数,超时返回0,失败返回-1
示例:见上面的 多路IO复用实现TCP并发服务器 的例子
4.使用setsockopt实现超时检测
①getsockopt()函数
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
功能:获取套接字的选项
#include <sys/types.h>
#include <sys/socket.h>
参数:
@sockfd:要操作的套接字
@level:
socket级别:SOL_SOCKET
tcp级别:IPPROTO_TCP
ip级别:IPPROTO_IP
@ optname:
socket级别:
SO_BROADCAST 是否允许发送广播
SO_RCVBUF 接收缓冲区的大小:单位字节
SO_REUSEADDR 设置端口复用
SO_SNDBUF 发送缓冲区的大小:单位字节
SO_RCVTIMEO 接收超时时间
SO_SNDTIMEO 发送超时时间
超时时间 optval参数 使用 struct timeval 结构体
超时会返回-1 并且错误码会被设置成 EAGAIN
@optval:socket级别,除非另有说明,否则是一个int *指针
@optlen:optval 大小
返回值: 成功返回0,失败返回-1,置位错误码
示例:使用getsockopt函数获取发送和接收缓冲区的大小
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(){
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
perror("socket error");
exit(-1);
}
int count = 0;
int len = sizeof(count);
if(-1 == getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF,&count, &len)){
perror("getsockopt error");
exit(-1);
}
printf("发送缓冲区大小 [%d]K\n", count/1024);
count = 0;
if(-1 == getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF,&count, &len)){
perror("getsockopt error");
exit(-1);
}
printf("接收缓冲区大小 [%d]K\n", count/1024);
return 0;
}
执行结果:发送缓冲区大小:16K 接收缓冲区大小:128K
②setsockopt函数说明:用法和 getsockopt 函数基本一样,只不过一个是获取一个是设置
示例:使用setsockopt设置端口复用
int sockfd = socket();
int on = 1;//设置端口复用时 optval是一个整数布尔型值 0 假 非0真
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
bind();
//端口复用要加在socket函数之后,bind之前
③超时检测代码实现:
//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
typedef struct MSG{
int acceptfd;
struct sockaddr_in client_addr;
}msg_t;
void *deal_recv_send(void *arg){
msg_t msg = *(msg_t *)arg;
printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port));
int bytes = 0;
char buff[128] = {0};
while(1){
//6.与客户端通信
//由已经设置过超时检测的sockfd产生的acceptfd会继承 sockfd 的超时属性
//如果不想改 直接使用即可
//如果想要重新设置,再次调用 setsockopt 即可
if(0 > (bytes = recv(msg.acceptfd, buff, 128, 0))){
if(errno == EAGAIN){
printf("recv tmeout\n");
break;//直接关闭客户端的套接字
}
ERRLOG("recv error");
}else if(bytes == 0){
printf("客户端 %s:%d 断开了连接\n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port));
break;
}else{
if(0 == strcmp(buff, "quit")){
printf("客户端 %s:%d 退出了\n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port));
break;
}
printf("%s-%d:[%s]\n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port), buff);
//组装应答
strcat(buff, "--sever");
if(-1 == send(msg.acceptfd, buff, 128, 0)){
ERRLOG("send error");
}
}
}
//7.关闭套接字
close(msg.acceptfd);
}
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
ERRLOG("listen error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&server_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
int acceptfd = 0;
pthread_t tid = 0;
msg_t client_msg;
//设置超时时间 5s
struct timeval tm;
memset(&tm, 0, sizeof(tm));
tm.tv_sec = 5;
tm.tv_usec = 0;
if(-1 == setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tm, sizeof(tm))){
ERRLOG("setsockopt error");
}
while(1){
//5.阻塞等待客户端连接
acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
if(-1 == acceptfd){
if(errno == EAGAIN){
printf("accept timeout\n");//自定义的处理方式 将此处的 printf替换掉即可
continue;
}
ERRLOG("accept error");
}
//将客户端的套接字和客户端的网络信息结构体传给线程处理函数
client_msg.acceptfd = acceptfd;
client_msg.client_addr = client_addr;
//创建线程单独处理和客户端的通信
if(0 != pthread_create(&tid, NULL, deal_recv_send, &client_msg)){
ERRLOG("pthread_create error");
}
//设置线程分离属性
if(0!=pthread_detach(tid)){
ERRLOG("pthread_detach error");
}
}
close(sockfd);
return 0;
}
//客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("connect error");
}
//4.与服务器通信
int bytes = 0;
char buff[128] = {0};
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
if(-1 == send(sockfd, buff, 128, 0)){
ERRLOG("send error");
}
if(0 == strcmp(buff, "quit")){
break;
}
if(-1 == (bytes = recv(sockfd, buff, 128, 0))){
ERRLOG("recv error");
}else if(bytes == 0){
//如果对端已经关闭了套接字
//第二次给对端发消息时 会出现 SIGPIPE 导致进程结束
printf("由于你长时间没有说话,已经被踢出聊天了\n");
break;
}
printf("收到回复:[%s]\n", buff);
}
//5.关闭套接字
close(sockfd);
return 0;
}
5.使用 alarm 闹钟实现超时检测
信号的自重启属性:使用alarm函数可以设置一个超时时间,一旦时间到达了,就会给进程发一个SIGALRM信号,进程对SIGALRM默认的处理方式是终止。对于服务器程序而言,不能因为超时就终止,所以需要对SIGALRM信号做一个捕捉。如果将信号的处理方式设置成捕捉,当信号产生时,就会去调用信号处理函数,当信号处理函数执行完毕后,程序会回到产生信号时的状态继续向下运行,这种属性称为信号的自重启属性。
如果想要使用alarm实现超时检测,就要关闭信号的自重启属性(sigaction函数)。关闭之后,信号处理函数执行完,会立即返回错误 EINTR,而不是重新启动原进程。
进程对信号默认的处理方式:
方式1:终止进程
方式2:终止进程
方式3:忽略
方式4:让停止的进程继续运行
人为对信号的处理方式:
方式1:忽略
方式2:默认
方式3:捕捉
sigaction函数说明:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:检查或者改变信号的行为
#include <signal.h>
参数:
@signum : 要处理行为的信号的编号,除了 SIGKILL 和 SIGSTOP
@act : 新的行为 (在获取行为时,可以置NULL)
@oldact : 旧的行为 (在设置行为时,可以置NULL)
struct sigaction {
void (*sa_handler)(int);//信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *);//信号处理函数 两个不要同时设置
sigset_t sa_mask;//关于阻塞的掩码 我们用不到
int sa_flags;//信号的行为
SA_RESTART 信号自重启属性
void (*sa_restorer)(void);//一般不用于应用程序
}
返回值: 成功返回0,失败返回-1,置位错误码
示例:使用sigaction关闭 SIGALRM 信号的自重启属性,并实现超时检测
//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
//自定义的信号处理函数
void my_signal(int x){
//什么都不用做
printf("my_signal\n");
}
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//取消SIGALRM信号的自重启属性
struct sigaction oldact;
memset(&oldact, 0, sizeof(oldact));
//获取旧的行为
if(-1 == sigaction(SIGALRM, NULL, &oldact)){
ERRLOG("sigaction error");
}
//设置信号处理函数
oldact.sa_handler = my_signal;
//取消自重启属性
oldact.sa_flags &= ~SA_RESTART;
//设置新的行为
if(-1 == sigaction(SIGALRM, &oldact, NULL)){
ERRLOG("sigaction error");
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
//网络字节序的端口号,可以是 8888 9999 6789 等都可以
server_addr.sin_port = htons(atoi(argv[2]));
//IP地址
//不能随便填,可以填自己主机的IP地址
//如果只是在本地测试,也可以填 127.0.0.1
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
ERRLOG("listen error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&server_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
char buff[128] = {0};
int acceptfd = 0;
int bytes = 0;
while(1){
alarm(5);//设置超时时间5s
//5.阻塞等待客户端连接
acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
if(-1 == acceptfd){
if(errno == EINTR){
printf("accept timeout\n");
continue;
}
ERRLOG("accept error");
}
printf("客户端 %s:%d 连接到服务器了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
while(1){
alarm(5);
//6.与客户端通信
if(0 > (bytes = recv(acceptfd, buff, 128, 0))){
if(errno == EINTR){
printf("recv timeout\n");
break;
}
printf("errno = %d\n", errno);
ERRLOG("recv error");
}else if(bytes == 0){
printf("客户端 %s:%d 断开了连接\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}else{
if(0 == strcmp(buff, "quit")){
printf("客户端 %s:%d 退出了\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
break;
}
printf("%s-%d:[%s]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
//组装应答
strcat(buff, "--sever");
if(-1 == send(acceptfd, buff, 128, 0)){
ERRLOG("send error");
}
}
}
//7.关闭套接字
close(acceptfd);
}
close(sockfd);
return 0;
}
//客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s <ip> <port>\n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
//2.填充服务器网络信息结构体
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("connect error");
}
//4.与服务器通信
char buff[128] = {0};
int bytes = 0;
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
if(-1 == send(sockfd, buff, 128, 0)){
ERRLOG("send error");
}
if(0 == strcmp(buff, "quit")){
break;
}
if(-1 == (bytes = recv(sockfd, buff, 128, 0))){
ERRLOG("recv error");
}else if(0 == bytes){
printf("由于你长时间没有说话,已经被踢掉了\n");
break;
}
printf("收到回复:[%s]\n", buff);
}
//5.关闭套接字
close(sockfd);
return 0;
}