C语言中IO模型实现并发服务器

本文详细介绍了C语言中实现并发服务器的IO模型,包括阻塞IO、非阻塞IO、IO多路复用,以及如何通过select、setsockopt和alarm实现网络超时检测。针对服务器模型,讲解了循环服务器、并发服务器的实现方式,如多线程、多进程和IO多路复用。最后,文章阐述了不同超时检测方法,如使用select和setsockopt设置超时,以及利用alarm进行超时管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值