Linux下网络编程
目录
1、同一台机器上调试---服务端使用本地ip:127.0.0.1
2、不同机器上调试----服务端使用另外一个ip地址:192.168.xxx.xxx
4、多进程服务端的实现(一个服务端可以接受多个客户端连接)---并发服务器
5、多进程服务端的实现(一个服务端可以接受多个客户端连接---并发服务器
2)select()函数原型、参数说明及select()工作过程
3)select()伪代码及使用select()实现服务端(一个进程实现多客户端连接)
2)使用epoll()实现服务端---该服务端使用一个进程可以接受多个客户端的连接
3)使用fcntl()将recv(fd,buf,sizeof(buf))中fd默认阻塞改变为非阻塞的方法及epoll的触发方式 fcntl()函数介绍
4)epoll()反应堆模型---主要改进是自己定义了一个结构体,并使epoll_data联合体中的ptr指向了这个结构体,而不是只使用了epoll_data中的fd
1、socket()---用于服务端和客户端
2、bind()---用于服务端 涉及到大端和小端模式
3、listen()---用于服务端
4、accetp()---用于服务端
5、connect()----用于客户端
8、inet_pton()----将点分十进制转换为网络传输用的数据个数
9、inet_ntop()----将网络传输用的数据个数转换为点分十进制格式
5、“bind error: Address already in use”错误解决方法
3.1 recv()中的参数设置(使用recv()的第三个参数决定是采用剪切方式还是以拷贝方式从缓冲区读数据)
3.2 使用正则表达式获取请求消息中浏览器要访问服务器资源的字符串---使用sscanf()
3.3 检查一个路径是文件还是目录用的函数---stat()
3.5 打开目录函数二:scandir() 注该函数使用起来较简单
3.6 浏览器、服务器中队函数的编码和解码的函数(自己定义)
一、相关协议的介绍
OSI七层模型
二、socket实现C/S通信
socket套接字的通信的本质:
1、同一台机器上调试---服务端使用本地ip:127.0.0.1
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/types.h> 5 #include <sys/stat.h> 6 #include <string.h> 7 #include <arpa/inet.h> 8 #include <ctype.h> 9 10 char server_ip[] = "127.0.0.1"; 11 int server_port = 8888; 12 13 int main(int argc, const char* argv[]) 14 { 15 // 创建监听的套接字 16 int lfd = socket(AF_INET, SOCK_STREAM, 0); 17 if(lfd == -1) //socket()失败则返回-1 18 { 19 perror("socket error"); 20 exit(1); 21 } 22 23 // lfd 和本地的IP port绑定 24 struct sockaddr_in server; 25 memset(&server, 0, sizeof(server)); 26 server.sin_family = AF_INET; // 地址族协议 - ipv4 27 server.sin_port = htons(server_port); 28 //server.sin_addr.s_addr = htonl(INADDR_ANY); //htons(a)表示将在主机中的小端存储方式转换为网络中的大端存储方式 s表示shrot 29 inet_pton(AF_INET,server_ip,&server.sin_addr.s_addr); //使用自己指定的ip 30 int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server)); //同理htonl() l表示long 31 if(ret == -1) 32 { 33 perror("bind error"); 34 exit(1); 35 } 36 37 // 设置监听 38 ret = listen(lfd, 20); //最多可监听的数目为20 39 if(ret == -1) 40 { 41 perror("listen error"); 42 exit(1); 43 } 44 45 // 等待并接收连接请求 46 struct sockaddr_in client; 47 socklen_t len = sizeof(client); 48 int cfd = accept(lfd, (struct sockaddr*)&client, &len); //如果没有客户端请求连接,则阻塞在这里,其中client为传出参数,包含了客户端的ip和端口号 49 if(cfd == -1) 50 { 51 perror("accept error"); 52 exit(1); 53 } 54 55 printf(" accept successful !!!\n"); 56 char ipbuf[64] = {0}; 57 printf("client IP: %s, port: %d\n", 58 inet_ntop(AF_INET, &client.sin_addr.s_addr, ipbuf, sizeof(ipbuf)), 59 ntohs(client.sin_port)); 60 // 一直通信 61 while(1) 62 { 63 // 先接收数据 64 char buf[1024] = {0}; 65 int len = read(cfd, buf, sizeof(buf)); 66 if(len == -1) 67 { 68 perror("read error"); 69 exit(1); 70 } 71 else if(len == 0) 72 { 73 printf(" 客户端已经断开了连接 \n"); 74 close(cfd); 75 break; 76 } 77 else 78 { 79 printf("recv buf: %s\n", buf); 80 // 转换 - 小写 - 大写 81 for(int i=0; i<len; ++i) 82 { 83 buf[i] = toupper(buf[i]); 84 } 85 printf("send buf: %s\n", buf); 86 write(cfd, buf, len); 87 } 88 } 89 90 close(lfd); 91 92 return 0; 93 }
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/types.h> 5 #include <sys/stat.h> 6 #include <string.h> 7 #include <sys/socket.h> 8 #include <arpa/inet.h> 9 10 int main(int argc,char* argv[]){ 11 12 if(argc < 2){ 13 printf("eg: ./a.out port\n"); 14 exit(1); //退出当前进程 15 } 16 int port = atoi(argv[1]); 17 18 //创建套接字 19 int fd=socket(AF_INET,SOCK_STREAM,0); 20 21 //连接服务器 22 struct sockaddr_in serv; 23 memset(&serv,0,sizeof(serv)); 24 serv.sin_family = AF_INET; 25 serv.sin_port = htons(port); 26 inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr); 27 connect(fd,(struct sockaddr*)&serv,sizeof(serv)); 28 29 //通信 30 while(1){ 31 //发送数据 32 char buf[1024]; 33 printf("请输入要发送的字符串:\n"); 34 fgets(buf,sizeof(buf),stdin); //读数据,读取用户在终端输入一个字符串 35 write(fd,buf,strlen(buf)); 36 37 //等待接受数据 38 int len = read(fd,buf,sizeof(buf)); 39 if(len == -1){ 40 perror("read erroe"); 41 exit(1); 42 } 43 else if(len == 0){ //对方已经关闭连接,此时read()不会发生阻塞 44 printf("服务端关闭了连接\n"); 45 break; 46 } 47 else{ 48 printf("reveive buf: %s\n",buf); 49 } 50 51 } 52 53 close(fd); 54 55 return 0; 56 }
调试结果:
2、不同机器上调试----服务端使用另外一个ip地址:192.168.xxx.xxx
检查一下你另外一台电脑的5150端口是否开放(CMD下TELNET IP 5150),要是开放的话在同一网段socket是能通信的,不同网段需要做NAT穿越,两台内网电脑的话考虑用P2P打洞来解决。
3、关于本地ip和对外ip
4、多进程服务端的实现(一个服务端可以接受多个客户端连接)
创建多进程服务器相关步骤和数据共享问题:
1 //创建多进程版服务器 2 3 /* 4 1、只能处理单连接服务器创建步骤 5 创建套接字 socket() 6 绑定 bind() 7 监听 listen() 8 接收连接请求 accept() 9 通信 connect() ---客户端发送connect()请求 10 2、能处理多连接服务器创建步骤 11 创建套接字 12 绑定 13 监听---listen(lfd,128); //最多可以监听128个客户端发送的连接请求 14 //以上均是一样的 15 接收连接请求 16 通信 17 父进程接收到客户端a发送的连接请求后,fork()出一个子进程1,子进程1负责与客户端a进行通信 18 之后父进程继续接收连接请求,如果父进程再收到客户端b发送的连接请求后,再fork()出一个子进程2,子进程2负责与客户端b进行通信; 19 之后父进程继续接收连接请求...... 20 21 使用多进程的方式,解决服务器处理多进程连接的问题: 22 1、读时共享,写时复制 23 假如父子进程中都有一个整形变量a: 24 (1)父子进程只对变量a做读操作时候,父进程中的变量a和子进程中的变量a对应物理内存中的同一块内存地址 25 (2)父进程对变量a进行写操作时,(如a=8),那么此时父进程中变量a在物理内存中的地址就改变了,将变量a复制到了物理内存中的另一块地址,父进程再次读a的时 26 父进程会去新的地址去读a,子进程还是去原来的地方去读a 27 (3) 即父进程fork()出一个子进程后,父进程中所有的变量都会在子进程中存在,并且在子进程中改变变量的值不会对父进程和其他子进程产生影响 28 (4)父子进程永远共享文件描述符、内存映射区(mmap) 29 2、父进程的角色? 30 等待接收连接(accept(),无连接时候阻塞),有连接时,fork()出一个子进程 31 3、子进程的角色? 32 使用accept()返回值(文件描述符)与客户端进行通信 33 4、关掉子进程中的监听文件描述符(listen()返回) 34 父进程创建完子进程后,子进程中的文件描述符和父进程中所有的文件描述符都是一样的,即在子进程中存在监听文件描述符 35 但是在子进程中的这个文件描述符并没有作用,所以此时为了节约内存,可以将子进程中的监听文件描述符关掉; 36 5、关掉父进程中的通信文件描述符(accept()返回) 37 父进程只用于接收连接请求,用不着用于通信的文件描述符,为节省开销,可关掉 38 39 40 41 6、创建进程个数的限制? 42 最多可以创建进程个数为cpu*2+2个 43 7、文件描述符个数限制? 44 文件描述符默认个数为1024个 45 8、子进程资源回收? 46 wait()或者是waitpid() 47 使用信号去回收 48 做信号(SIGCHLD)捕捉signal()或者是sigaction() 49 50 */
多进程伪代码:
1 /*创建多进程版服务器伪代码 2 3 void recycle(int num){ 4 while((waitpid(-1,NULL,WNOHANG) > 0); 5 } 6 7 int main(){ 8 //创建套接字 9 int lfd=socket(); 10 //绑定 11 bind() 12 //监听 13 listen(); 14 15 //使用信号回收子进程中的资源 16 struct sigaction act; 17 act.flags = 0; 18 sigemptyset(&act.sa_mask); //将结构体中的sa_mask清空 19 act.sa_handler = recycle; //recycle为SIGCHLD绑定的动作 20 sigaction(SIGCHLD,&act,NULL); 21 22 //父进程 23 while(1){ 24 int cfd=accept(); 25 26 //创建子进程 27 pid_t pid=fork(); 28 if(pid==0){ 29 //son 30 close(lfd); //父进程中关闭用于监听的文件描述符,以节省资源 31 //通信 32 while(1){ 33 int len=read(); 34 if(len=-1){ //read()出错 35 exit(1); 36 } 37 else if(len == 0){ //客户端关闭连接 38 close(cfd); 39 break; 40 } 41 else{ //read()正常 42 write(); 43 } 44 } 45 return 0; //通信完毕后退出子进程 46 } 47 else{ 48 //parent 49 close(cfd); //父进程中关闭用于通信的文件描述符,以节省资源 50 //使用信号回收子进程中的资源 51 while(waitpid(-1,NULL,WNOHANG)) 52 } 53 } 54 }
多进程服务端实现
1 //process_server.c 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <arpa/inet.h> 9 #include <ctype.h> 10 #include <signal.h> 11 #include <sys/wait.h> 12 #include <errno.h> //for EINTR 13 14 char server_ip[] = "127.0.0.1"; 15 int server_port = 8888; 16 17 void sig_usr(int num) 18 { 19 pid_t pid; 20 //一定要使用循环去回收子线程,因为可能有好几个线程同时结束 21 while((pid = waitpid(-1,NULL,WNOHANG)) == -1) 22 { 23 printf("chid died,pid=%d\n",pid); 24 } 25 } 26 27 int main(int argc, const char* argv[]) 28 { 29 // 创建监听的套接字---socket() 30 int lfd = socket(AF_INET, SOCK_STREAM, 0); 31 if(lfd == -1) //socket()失败则返回-1 32 { 33 perror("socket error"); 34 exit(1); 35 } 36 37 // lfd 和本地的IP port绑定---bind() 38 struct sockaddr_in server; 39 memset(&server, 0, sizeof(server)); 40 server.sin_family = AF_INET; // 地址族协议 - ipv4 41 server.sin_port = htons(server_port); 42 //server.sin_addr.s_addr = htonl(INADDR_ANY); //htons(a)表示将在主机中的小端存储方式转换为网络中的大端存储方式 s表示shrot 43 inet_pton(AF_INET,server_ip,&server.sin_addr.s_addr); //使用自己指定的ip 44 int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server)); //同理htonl() l表示long 45 if(ret == -1) 46 { 47 perror("bind error"); 48 exit(1); 49 } 50 51 // 设置监听---listen() 52 ret = listen(lfd, 36); //最多可监听的数目为36 53 if(ret == -1) 54 { 55 perror("listen error"); 56 exit(1); 57 } 58 59 //使用信号回收子进程中的资源(pcb) 60 struct sigaction act; 61 act.sa_handler = sig_usr; 62 sigemptyset(&act.sa_mask); 63 //sigaddset(&act.sa_mask, SIGUSR2); //这一句不加也是可以得,因为sa_mask默认将当前信号本身阻塞 64 act.sa_flags = 0; 65 sigaction(SIGCHLD, &act, NULL); 66 67 struct sockaddr_in client_addr; 68 socklen_t cli_len=sizeof(client_addr); 69 while(1) 70 { 71 //父进程接收连接请求---accept() 72 //如果accept()阻塞的时候被信号中断,处理完信号对应的操作之后,回来之后就不阻塞在accept()这里了,accept()直接返回-1,此时errno被设置成EINTR 73 //解决方法是在if中重新调用accept()即可 74 int cfd = accept(lfd,(struct sockaddr*)&client_addr,&cli_len); 75 if(cfd==-1 && errno == EINTR) 76 { 77 cfd = accept(lfd,(struct sockaddr*)&client_addr,&cli_len); //防止accept()被信号打断 78 } 79 //创建子进程 80 pid_t pid=fork(); 81 82 //如果是子进程 83 if(pid==0) 84 { 85 //son---用于通信 86 close(lfd); //在子线程中关闭用于监听的文件描述符lfd,以节省资源 87 88 char client_ip[64]; 89 int clientPort; 90 while(1) 91 { 92 //打印客户端中的ip和端口 93 inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,client_ip,sizeof(client_ip)); //将客户端的IP保存在ip这边char型数组中 94 clientPort = ntohs(client_addr.sin_port); //获取客户端端口 95 printf("client IP:%s,PORT:%d\n",client_ip,clientPort); 96 97 char buf[1024] = {1024}; //接收数据缓冲,这里必须将buf初始化为0,否则在服务端打印接受到的字符的时候回出现乱码 98 int len = read(cfd,buf,sizeof(buf)); 99 if(len == -1) 100 { 101 perror("read err"); 102 exit(1); 103 } 104 else if(len == 0) 105 { 106 printf("客户端断开了连接\n"); 107 close(cfd); 108 break; 109 } 110 else 111 { 112 printf("recv buf:%s\n",buf); 113 write(cfd,buf,len); //再给客户端发过去,len为读到数据的长度 114 } 115 } 116 //跳出了while循环,不再进程通信了 117 return 0; //结束子进程 exit(1)也是可以得 118 } 119 120 //如果是父进程 121 else if(pid > 0) 122 { 123 //parent 124 close(cfd); //cfd文件描述符是用于通信的,但是父进程不用于通信,所以在父进程中关闭即可 125 } 126 } 127 close(lfd); 128 return 0; 129 }
客户端依次断开连接:
5、多进程服务端的实现(一个服务端可以接受多个客户端连接)
创建多线程服务器相关步骤和数据共享问题:
1 /* 2 1、只能处理单连接服务器创建步骤 3 创建套接字 socket() 4 绑定 bind() 5 监听 listen() 6 接收连接请求 accept() 7 通信 connect() ---客户端发送connect()请求 8 2、能处理多连接服务器创建步骤 9 创建套接字 10 绑定 11 监听---listen(lfd,128); //最多可以监听128个客户端发送的连接请求 12 //以上均是一样的 13 接收连接请求 cfd=accept(); //cfd是一个文件文件描述符,用于服务端和客户端之间进行通信 14 通信 15 主线程接收到客户端a发送的连接请求后,主线程thread_creat()出一个子线程1,并将用于通信的文件描述符cfd通过thread_creat()的第四个形参传递给子线程1 16 之后主线程继续接收连接请求,如果主线程再收到客户端b发送的连接请求后,主线程thread_creat()出一个子线程2,并将用于通信的文件描述符cfd通过thread_creat()的第四个形参传递给子线程2; 17 之后主线程继续接收连接请求...... 18 */
多线程伪代码:
子线程和父线程之间共享进程的内存,数据是共享的,所以父进程将一个用于通信的文件cfd描述符传递给子线程1后,父线程再次接收客户端连接时候,此时cfd的值会改变,传给子线程1的cfd也会改变,所以应该创建一个数组来保存用于通信的cfd,但是子线程自行可能需要客户端的一些信息,所以可以创建一个结构体来保存文件描述符、客户端ip、port等信息,并将次结构题传递给子线程。
1 void* worker(void* arg){ 2 while(1) 3 { 4 read(); 5 write(); 6 } 7 } 8 9 int main(){ 10 //创建套接字 11 int lfd=socket(); 12 //绑定 13 bind() 14 //监听 15 listen(); 16 17 //使用信号回收子进程中的资源 18 struct sigaction act; 19 act.flags = 0; 20 sigemptyset(&act.sa_mask); //将结构体中的sa_mask清空 21 act.sa_handler = recycle; //recycle为SIGCHLD绑定的动作 22 sigaction(SIGCHLD,&act,NULL); 23 24 //主线程 25 while(1){ 26 //int cfd=accept(lfd,&cliet,&len); //cfd存储在栈上,假如主线程第一次接收一个客户端请求的时候,cfd=4,之后将这个4传递给子线程1函数1 27 //在子线程1还没有结束时,主线程又接收了一个客户端请求,此时cfd=5,然后创建子线程2,此时子线程1中的cfd也等于5了 28 //解决方法是对于文件描述符使用数组来存储,但是假如要在线程中答应客户端ip和port的信息,需要将cliet的信息也传递到子线程中去,所以可以使用结构体数组 29 //将结构体数组中的一个元素传递到子线程中去 30 //由于子线程函数中需要将arg进行强制转换为Sockinfo类型的,所以这个结构体需要放到全局区 31 typrdef struct sockinfo 32 { 33 pthread_t tid; 34 int fd; 35 struct sockaddr_in addr; 36 }Sockinfo; 37 Sockinfo sock[256]; //创建包含与客户端通信的文件描述符和客户端信息的结构体数组 38 39 //接收连接请求 40 sock[i].fd = accept(lfd,&cliet,&len); 41 42 //创建子线程 43 pthread_t tid; 44 pthread_create(&sock[i].tid,NULL,worker,&sock[i]); //将sock的地址传递进去 45 46 //主线程回收子线程 47 pthread_detach(sock[i].tid); 48 } 49 }
1 #include ... 2 3 typrdef struct sockinfo{ 4 pthread_t tid; 5 int fd; 6 struct sockaddr_in addr; 7 }Sockinfo; 8 9 10 void* worker(void* arg){ 11 while(1){ 12 read(); 13 write(); 14 } 15 } 16 17 int main(){ 18 //创建套接字 19 int lfd=socket(); 20 //绑定 21 bind() 22 //监听 23 listen(); 24 25 //使用信号回收子进程中的资源 26 struct sigaction act; 27 act.flags = 0; 28 sigemptyset(&act.sa_mask); //将结构体中的sa_mask清空 29 act.sa_handler = recycle; //recycle为SIGCHLD绑定的动作 30 sigaction(SIGCHLD,&act,NULL); 31 32 //主线程 33 Sockinfo sock[256]; //创建包含与客户端通信的文件描述符和客户端信息的结构体数组 34 while(1){ 35 //接收连接请求 36 sock[i].fd = accept(lfd,&cliet,&len); 37 38 //创建子线程 39 pthread_t tid; 40 pthread_create(&sock[i].tid,NULL,worker,&sock[i]); //将sock的地址传递进去 41 42 //主线程回收子线程 43 pthread_detach(sock[i].tid); 44 } 45 }
1 //pthread_server.c 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <arpa/inet.h> 9 #include <ctype.h> 10 #include <signal.h> 11 #include <sys/wait.h> 12 #include <errno.h> //for EINTR 13 #include <pthread.h> 14 15 char server_ip[] = "127.0.0.1"; 16 int server_port = 8888; 17 18 //每一个线程都需要一个内存地址区里的文件描述符和客户端信息,故使用结构体数组 19 typdef struct sockinfo{ 20 pthread_t tid; //存放线程id 21 int fd; //存储用于通信的文件描述符 22 struct sockaddr_in addr; //存放客户端信息(ip和port) 23 }SockInfo; 24 25 //子线程 26 void* worker(void* arg){ 27 SockInfo* info = (SockInfo*)arg; 28 //通信 29 char client_ip[64]={0}; 30 int client_port; 31 char buf[1024]={0}; 32 while(1){ 33 inet_ntop(AF_INET,&info->addr.sin_addr.s_addr,client_ip,sizeof(client_ip)); //将客户端的IP保存在client_ip中 34 client_port = ntohs(info->addr.sin_port); //将客户端的端口保存在client_port中 35 printf("Client IP:%s,Port:%d\n",client_ip,client_port); 36 37 int len = read(info->fd,buf,sizeof(buf)); //将客户端发送的数据通过文件描述符fd读到buf中 38 if(len == -1){ 39 perror("read err"); 40 pthread_exit(NULL); //只退出单个线程,对其他线程没有影响 41 } 42 else if(len == 0){ 43 printf("客户端:%d断开了连接\n",client_port); 44 close(info->fd); 45 break; 46 } 47 else{ 48 printf("client:%d message:%s\n",client_port,buf); 49 write(info->fd,buf,len); //再给客户端写回去 50 buf[0] = '\0'; //清空上次传输遗留下来的数据 51 } 52 } 53 54 return NULL; 55 } 56 57 58 int main(int argc, const char* argv[]){ 59 // 创建监听的套接字 60 int lfd = socket(AF_INET, SOCK_STREAM, 0); 61 if(lfd == -1){ //socket()失败则返回-1s 62 perror("socket error"); 63 exit(1); 64 } 65 66 // lfd 和本地的IP port绑定 67 struct sockaddr_in server; 68 memset(&server, 0, sizeof(server)); 69 server.sin_family = AF_INET; // 地址族协议 - ipv4 70 server.sin_port = htons(server_port); 71 //server.sin_addr.s_addr = htonl(INADDR_ANY); //htons(a)表示将在主机中的小端存储方式转换为网络中的大端存储方式 s表示shrot 72 inet_pton(AF_INET,server_ip,&server.sin_addr.s_addr); //使用自己指定的ip 73 int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server)); //同理htonl() l表示long 74 if(ret == -1){ 75 perror("bind error"); 76 exit(1); 77 } 78 79 // 设置监听 80 ret = listen(lfd, 36); //最多可监听的数目为36 81 if(ret == -1){ 82 perror("listen error"); 83 exit(1); 84 } 85 86 socklen_t cli_len=sizeof(struct sockaddr_in); 87 88 //每一个线程都需要一个内存地址区里的文件描述符和客户端信息,故使用结构体数组 89 SockInfo info[256]; //可以容纳256个线程信息 90 //fd=-1表示该文件描述符无效,此时需要将结构体数组中的所有文件描述符初始化为无效的 91 for(int j=0;j<sizeof(info)/sizeof(info[0]);++j){ 92 info[j].fd = -1; 93 } 94 int i; 95 while(1){ 96 //选一个没有被使用的,且最小的i,目的是中间有客户端退出(线程退出),可重复使用已退出了的fd 97 for(i=0;i<sizeof(info)/sizeof(info[0]);++i){ 98 if(info[i].fd)==-1){ 99 break; //跳出for循环,找到了一个没有使用的fd且i最小 100 } 101 } 102 if(i == 256){ 103 break; //跳出while()循环 104 } 105 106 //主线程---等待接收客户端发送的连接请求 107 info[i].fd = accept(lfd,(struct sockaddr*)&info[i].addr,&cli_len); 108 109 //创建子线程,子线程去做通信的操作 110 pthread_create(&info[i].tid,NULL,worker,&info[i]); 111 112 //设置线程分离---子线程在结束的时候,自动释放子线程中的资源 113 pthread_detach(info[i].tid); 114 } 115 116 pthread_exit(NULL); //只退出主线程 117 118 } 119 120 //注意pthread不是LInux下的默认库,编译的时候需要加上-g -lpthread 121 //gcc pthread_server.c -g -lpthread -o pthread_server
注意pthread不是LInux下的默认库,编译的时候需要加上-g -lpthread
gcc pthread_server.c -g -lpthread -o pthread_server
测试结果:
6、进程和线程的区别
/* 进程与线程的区别: 1、线程共享内存空间,进程的内存是独立的 2、子线程和父线程之间共享进程的内存,数据是共享的,子进程和父进程克隆了一份内存,数据是独立的 3、同一个进程的线程之间可以直接交流,如果两个进程想通信,必须通过一个中间代理来实现 4、创建新线程很简单,创建新进程需要对其父进程进行一次克隆 5、一个线程可以控制和操作同一进程里的其他线程。但是进程只能操作子进程 6、对主线程的修改可能影响其他线程的行为,对父进程的修改不影响子进程 7、删除线程不影响同一进程里的其他线程,如果kill父进程,子进程也跟着没了 */
7、I/O多路转接
1)基本概念以及为啥I/O多路转接优于多进程和多线程
I/O复用原理:让应用程序可以同时对多个I/O端口进行监控以判断其上的操作是否可以进行,达到时间复用的目的。
在整个程序运行的过程中一次I/O主要花费在哪个步骤?没错,就是等待数据就绪的步骤。那么,我们下面所提出的I/O多路转接复用就是将等待数据就绪的过程单独拿了出来进行性能上的优化。提出I/O多路转接复用的目的,其实就是为了提高服务器的吞吐能力,大大优化服务性能
在书上看到一个例子来解释I/O的原理,我觉得很形象,如果用监控来自10根不同地方的水管(I/O端口)是否有水流到达(即是否可读),那么需要10个人(即10个线程或10处代码)来做这件事。如果利用某种技术(比如摄像头)把这10根水管的状态情况统一传达到某一点,那么就只需要1个人在那个点进行监控就行了,而类似与select或epoll这样的多路I/O复用机制就好比是摄像头的功能,它们能够把多个I/O端口的状况反馈到同一处,比如某个特定的文件描述符上,这样应用程序只需利用对应的select()或epoll_wait()系统调用阻塞关注这一处即可。
1 /* 2 (1)假如有10个客户端与服务端建立了连接,那么就需要10个进程或者是10个线程来处理读写操作,所有监听、连接操作都由自定义的应用程序来处理,并且会发生read()阻塞的情况; 3 (2)select或者是epoll就类似于一个客户端(I/O)监管,在哪个客户端有消息发送过来的时候,通知应用程序去处理这个消息,并且select使用内核来处理监听和连接操作,然后给应用程序一个反馈 4 */
1 /* 2 使用多线程和多进程也能够实现多个客户端和服务器的数据收发的功能。那么所有的监听连接请求和请求的接受处理都会由服务器来处理,即用户自定义的程序来处理(就是我们自己编写的server.c的代码了),这在大型的项目,是极其不推荐的,它不仅降低了程序执行的效率,而且还占用了大量的CPU资源(即accept去监听)。 3 4 用户的应用来处理。若使用多路I/O转接,那么 这里关于监听和连接的不再是由用户自己处理,取而代之由内核应用程序监视文件。(即把内核请过来当帮手,来监听相应的客户端,当内核接受客户端的连接请求,给应用程序(server.c)一个反馈,然后应用程序立即与客户端建立连接(不需要再去accept()等待,此部分为链接的部分),然后把它继续放到内核里面去监听,当再次反馈给应用程序的时候,就是有数据过来需要读取了(就不在需要read()的等待)。不然应用程序会在accept(),read()部分会被阻塞。这里使用这种机制,可以当有这些事件发生了,应用程序才去处理,这就空出了很多的时间,不再是阻塞,应用程序可以去干其他的事情。整个机制类似于signal()的功能) 5 6 内核反馈的时候是一定有事件发生了,具体是什么事件,建立连接,读事件或者写事件,应用程序处理的时候就需要去具体区分。 7 8 这就是使用整个多路IO与多进程多线程的简单区别。 9 */
2)select()函数原型、参数说明及select()工作过程
select()头文件、函数原型机参数说明
1 #include <sys/select.h> 2 int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout) 3 4 nfds: 要检测的文件描述符中(readfds,writefds,exceptfds)最大的文件描述符加1;加1是由于内核有一个for循环遍历这个文件描述符集,for循环中的i是从0开始的,所以要遍历整个文件描述符数组,要将数组内元素个数+1 5 即内核中遍历文件描述符数组的for循环为for(int i=0;i<文件描述符个数+1;i++){ //readfds[i]...} 6 readfds:只检测读缓冲区( 能检测的最多的文件描述符为1024个)。由于接受数据是一个被动行为,自己就不知道对方啥时候给发数据,需要一直去检测读缓冲区 7 writefds: 只检测写缓冲区。由于写操作是主动的,所以一般也不关注 8 exceptfds:只检测文件描述符是否发生了异常。异常集合(一般不关心异常,直接赋值为NULL即可) 9 timeout:timeout一定要给tv_sec和tv_usec分别赋值,因为如果不赋值就是一个随机数 10 如果timeout赋值为NULL表示select()永久阻塞, 11 当检测到文件描述符发生变化的时候 ,select不阻塞,(select执行完毕) 12 如果timeout不为空 13 timeval a 14 a.tv_sec = 10; 15 a.tv_usec = 0; 16 表示select阻塞10s,10s之后不管读缓冲区有没有数据,select()都会返回(不阻塞) 17 18 19 struct timeval{ 20 long tv_sec; 21 long tv_usec; 22 23 }; 24 返回值为:传入的文件描述符中状态发生改变了的文件描述符的个数
select()中使用的fd_set在内核中使用数组实现,既然是数组,需要指定大小,于是内核指定的这个大小为1024,(1024单位为位(标志位))
故select局限性为做多能检测1024个进程或线程
操作自定义描述符标志位函数:
1 void FD_ZERO(fd_set *set); //将set中的文件描述符标志位清空为0 2 void FD_CLR(int fd,fd_set *set); //删除set中的fd,fd对应的标志位从1变为0 3 void FD_SET(int fd,fd_set *set); //将set中fd对应的标志位找位置,并设置对应的标志位为1 4 int FD_ISSET(int fd,fd_set *set); //判断set中的文件描述符fd对应的标志位是否为1;标志位为1则返回1,否则返回0
select的工作过程
3)select()伪代码及使用select()实现服务端(一个进程实现多客户端连接)
使用select()实现一个服务器,该服务器使用一个进程并可以接收多个客户端发送的连接请求
1 /* 2 select伪代码---一个进程可以搞定多个客户端的连接 3 4 int main(){ 5 int lfd = socket(); //用于监听 6 bind(); 7 listen(); 8 9 //创建一个文件描述符表,temp用于备份readfds 10 fd_set readfds,temp; 11 FD_ZERO(&readfds); //将readfds文件描述符中的标志位全部初始化为0 12 13 FD_SET(lfd,&readfds); //将监听的文件描述符添加到读集合readfds中 14 15 while(1){ 16 //委托内核去检测 17 temp = readfds; 18 int ret = select(maxfd+1,&temp,NULL,NULL,NULL); //在temp中有文件描述符发生改变,则select停止阻塞 19 20 //判断是不是监听的文件描述符发生改变了(即判断是否有新的客户端发送了连接请求) 21 if(FD_ISSET(lfd,&temp)){ //判断用于监听的文件描述符lfd的标志位在temp中是否发生了改变 22 23 //接收新的连接请求 24 int cfd = accept(); //此时accept()不足阻塞等待,因为此时一定有新的连接请求 25 26 //将新的用于通信的文件描述符添加到readfds中 27 FD_SET(cfd,&readfds); 28 //更新maxfd 29 maxfd = maxfd < cfd ? cfd : maxfd; 30 } 31 //已连接上的客户端向服务端发送数据 32 //上面已经判断了lfd在这次循环中是由于接收了连接请求后标志位才发生改变的,这里的lfd标志位改变是因为有新的客户端请求(不是客户端发送数据),所以直接判断lfd后一个文件描述符即可 33 //上面更新maxfd中已经将cfd赋值给maxfd了,所以maxfd也是可以访问的文件描述符 34 for(int i=lfd+1;i<=maxfd;++i){ 35 if(FD_ISSET(i,&temp)){ //如果文件描述符i在temp中的标志位发生了改变 36 int len = read(); 37 if(len == 0){ 38 //说明客户端已经断开了连接 39 FD_CLR(i,&readfds); //从读集合中删除文件描述符i 40 } 41 write(); 42 } 43 } 44 } 45 } 46 */
使用select()实现服务端,该服务端使用一个进程和一个线程,并可以接受多个客户端的的连接请求及数据通信
1 //select_server.c 只有一个进程也支持多客户端连接 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <arpa/inet.h> 9 #include <ctype.h> 10 #include <signal.h> 11 #include <sys/wait.h> 12 #include <errno.h> //for EINTR 13 14 char server_ip[] = "127.0.0.1"; 15 int server_port = 8888; 16 17 int main(int argc, const char* argv[]){ 18 // 创建监听的套接字 19 int lfd = socket(AF_INET, SOCK_STREAM, 0); 20 if(lfd == -1){ //socket()失败则返回-1s 21 perror("socket error"); 22 exit(1); 23 } 24 25 // lfd 和本地的IP port绑定 26 struct sockaddr_in server; 27 memset(&server, 0, sizeof(server)); 28 server.sin_family = AF_INET; // 地址族协议 - ipv4 29 server.sin_port = htons(server_port); 30 //server.sin_addr.s_addr = htonl(INADDR_ANY); //htons(a)表示将在主机中的小端存储方式转换为网络中的大端存储方式 s表示shrot 31 inet_pton(AF_INET,server_ip,&server.sin_addr.s_addr); //使用自己指定的ip 32 int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server)); //同理htonl() l表示long 33 if(ret == -1){ 34 perror("bind error"); 35 exit(1); 36 } 37 38 // 设置监听 39 ret = listen(lfd, 36); //最多可监听的数目为36 40 if(ret == -1){ 41 perror("listen error"); 42 exit(1); 43 } 44 45 //创建接收到的客户端信息数据结构数组,这是由于在服务端只有一个进程,每次都会覆盖掉客户端的信息 46 struct sockaddr_in client_info; 47 socklen_t cli_len = sizeof(struct sockaddr_in); 48 49 //定义select形参maxfd,并将mafd初始化为用于监听的文件描述符ulfd 50 int maxfd = lfd; 51 //文件描述符读集合,temp为实际传入select参数,以保证rdset不会被破坏 52 fd_set rdset,temp; 53 //rdset初始化 54 FD_ZERO(&rdset); 55 FD_SET(lfd,&rdset); //将监听的文件描述符lfd放在rdset读集合中 56 57 //已连接了的客户端数目 58 int ClientNum=-1; 59 while(1){ 60 //委托内核做IO的检测 61 temp = rdset; 62 int ret = select(maxfd+1,&temp,NULL,NULL,NULL); 63 if(ret == -1){ 64 perror("select error"); 65 exit(1); 66 } 67 //有客户端发起了新连接,即用于监听的文件描述符lfd在temp中对应的标志位被置为1 68 if(FD_ISSET(lfd,&temp)){ 69 //接收新的连接请求 70 //已连接了的客户端数目加1 71 ++ClientNum; 72 int cfd = accept(lfd,(struct sockaddr*)&client_info,&cli_len); 73 if(cfd == -1){ 74 perror("accept error"); 75 exit(1); 76 } 77 //accept()成功,则cfd对应一个新的客户端,将cfd加入到待检测的读集合 78 FD_SET(cfd,&rdset); 79 //更新最大的文件描述符 80 maxfd = maxfd < cfd ? cfd : maxfd; 81 82 //打印已经连接了的新的客户端的ip和端口 83 char client_ip[64]; //用于打印 84 int client_port; //用于打印 85 inet_ntop(AF_INET,&client_info.sin_addr.s_addr,client_ip,sizeof(client_ip)); 86 cilent_port = ntohs(client_info.sin_port); 87 printf("new client ip:%s,port:%d\n",client_ip,client_port); 88 } 89 //已经连接的客户端给我发送数据 90 for(int i=lfd+1;i<=maxfd;++i){ 91 //判断文件描述符i在temp中对应的标志位是否被置为了1 92 if(FD_ISSET(i,&temp)){ 93 char buf[1024] = {0}; 94 int len = recv(i,buf,sizeof(buf),0); 95 if(len == -1){ 96 perror("recv error"); 97 exit(1); 98 } 99 else if(len == 0){ 100 printf("客户端已经断开了连接\n"); 101 close(i); 102 FD_CLR(i,&rdset); //从原始表中删除已断开连接客户端的文件描述符i 103 } 104 else{ 105 printf("recv buf:%s\n",buf); 106 send(i,buf,sizeof(buf)-1,0); //在recv()的时候会把终端输入的换行符也接收进来,所以发送的时候要去掉,如果换成+1会导致客户端退出时服务端进程退出并发出"Connection reset by peer" 107 } 108 } 109 } 110 } 111 112 close(lfd); 113 return 0; 114 115 }
测试结果:
select局限性
01)最多可以接收1024个客户端的连接和通信请求,内核使用数组来存储文件描述符表,这个数组初始化的时候被初始化为1024个元素
02)文件描述符在用于自定义程序和内核之间来回拷贝,在fd很多的时候开销会很大
优点:可以跨平台
8、poll()函数介绍
poll()函数参数及返回值介绍
1 /* 2 poll()函数:内部实际上使用链表实现 3 4 #include <poll.h> 5 int poll(struct pollfd *fd,nfds_t nfds,int timeout); 6 pollfd: 数组的地址,将select()中的文件描述符读集合、写集合、异常集合封装在了一个结构体中 7 8 9 struct pollfd{ 10 int fd; //文件描述符 11 short enents; //等待的事件 short占16位 12 short revents; //实际发生的事件 内核给的反馈(反馈是什么事件) 13 }; 14 events可以被赋值的读事件为: 15 POLLIN: 普通或优先带数据可读 16 POLLRDNORM: 普通带数据可读 17 POLLRDBAND: 优先级带数据可读 18 POLLPRI: 高优先级数剧可读 19 events可以被赋值的写事件为: 20 POLLOUT:普通或优先带数据可写 21 POLLWRNORM: 普通带数据可写 22 POLLWRBAND: 高优先级数剧可写 23 events可以被赋值的异常事件为: 24 POLLERR: 发生错误 25 POLLHUP: 发生挂起 26 POLLNVAL: 描述不是打开的文件 27 nfds:一个int型的数,数组中最后一个使用的元素下标加1 28 例如: 29 sturct pollfd all[20]; 30 all[0].fd = 3; 31 all[1].fd = 4; 32 all[2].fd = 5; 33 all[3].fd = 6; 34 all[4].fd = 7; 35 那么nfds = 5,然后将nfds+1传递给内核的maxfd,内核做以下遍历: 36 for(int i=0;i<maxfd;++i){ ... } 37 timeout: -1 永久阻塞 38 0 调用完立即返回 39 >0 等待的时长 40 41 返回值:IO发生变化的文件描述符的个数 42 43 */
9、I/O多路转接---epoll()实现
1)相关函数介绍
epoll相关函数
1 /* 2 epoll相关函数 3 4 //生成一个epoll专用的文件描述符,生成一个树的根节点 5 int epoll_create(int size); 6 size: epoll上能关注的最大描述符数目,epoll在二叉树上挂size个节点,但是在后来超过size个节点,epoll也会自动进行扩展 7 返回值即为在文件描述符对应的数值 8 //用于控制某个epoll文件描述符事件,可以注册、修改、删除 9 int epoll_ctl(int epfd,int op,inf fd,struct epoll_event *event); 10 epfd:二叉树的根节点,即epoll_create()返回值 11 op对应的宏定义: 12 EPOLL_CTL_ADD---注册 13 EPOLL_CTL_MOD---修改 14 EPOLL_CTL_DEL---删除 15 fd:通过op参数要注册或修改或删除的文件描述符 16 event:告诉内核要监听什么事件,主要是对第三个参数fd的描述,event中可以包含fd和fd对应的事件(假如使用了联合体中的fd这个数据) 17 之后epoll_ctl会将event中的信息拷贝一份挂到二叉树上,因此除了头节点外的其余节点的数据类型都为结构体类型(epoll_event), 18 该结构体中保存了文件描述符和对应的事件 19 20 21 struct epoll_event{ 22 uint32_t events; 23 epoll_data_t data; 24 }; 25 其中: 26 events: 27 EPOLLIN---读事件 28 EPOLL_OUT---写事件 29 EPOLL_ERR---异常事件 30 data是一个联合体:共用同一块内存空间,即在赋值的时候只需要给其中的一个赋值就可以了 31 常用联合体中的fd参数,因为一看fd就是一个文件描述符 32 typedef union epoll_data{ 33 void *ptr; //如果使用联合体中的ptr,则可以定义一个结构体,让ptr指向这个结构体的地址,在结构体中可以定义包含fd的更多信息 34 int fd; //如果使用fd,则在epoll_ctl()中的最后一个参数(即挂在二叉树上的节点)信息只包含fd这一个信息 35 uint32_t u32; 36 uint64_t u64; 37 }epoll_data_t; 38 39 可以自定义一个结构体,让ptr指向这个结构体的地址,而结构体中可以包含自定的event中应该要包含什么样的信息, 40 返回值:成功返回0,失败返回-1 41 42 //等待I/O事件发生对应select()和poll() 43 int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout); 44 45 参数说明: 46 epfd:树的根节点,即epoll_create()的返回值 47 events:当二叉树上的某个节点的fd对应的event(事件)发生,那么就将这个节点拷贝到epoll_wait()中的events 48 maxevents:告诉内核这个events的大小,即events数组大小 49 timeout: 50 -1:一直阻塞 51 0:立即返回 52 >0: 多少毫秒后停止阻塞并返回 53 */
epoll()实现服务端伪代码
1 int mian(){ 2 //创建监听套接字 3 int lfd = socket(); 4 //绑定 5 bind() 6 //监听 7 listen() 8 9 //epoll树根节点---epoll_wait()第一个参数 10 int epfd = epoll_create(3000); //3000表示树上节点个数最多为3000,但是超过了这个数也可以,会自动进行扩展 11 //存储发生变化的fd对应的信息---epoll_wait()第二个参数 12 struct epoll_event all[3000]; 13 //二叉树上结点创建并初始化---将监听的文件描述lfd符挂在树上---epoll_ctl()第四个参数 14 struct epoll_event ev; //树上节点数据类型是struct epoll_event,所以要新建一个结构体来包含文件描述符fd和fd对应的时间信息 15 ev.events = EPOLLIN; 16 ev.data.fd = lfd; //使用data联合体中的fd 17 epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); 18 19 while(1){ 20 //委托内核检测事件,返回值ret为fd发生变化的个数,并将发生变化的fd存储在all中 21 int ret = epoll_wait(epfd,all,3000,-1); //-1表示一直阻塞 22 //根据ret遍历all数组 23 for(int i=0;i<ret;++i){ 24 int fd = all[i].daya.fd; 25 //有新的连接 26 if(fd == lfd){ 27 //接收连接请求 28 int cfd = accept(); 29 //将cfd挂在二叉树上 30 ev.events = EPOLLIN; 31 ev.data.fd = cfd; 32 epoll_crl(epfd,EPOLL_CTL_ADD,cfd,&ev) 33 } 34 //已连接了的客户端发送数据过来 35 else{ 36 //只处理客户端发来的数据 37 if(!all[i].events & EPOLLIN){ //如果all[i].events中不包含EPOLLIN事件 38 continue; 39 } 40 //读数据 41 int len = recv(); 42 if(len == 0){ 43 close(fd); 44 //将fd从树上删除 45 epoll_ctl(epfd,EPOLL_STL_DEL,fd,NULL); //删除的时候没有必要对ev初始化 46 } 47 //写数据 48 write(); 49 } 50 } 51 } 52 }
epoll原理:
2)使用epoll()实现服务端---该服务端使用一个进程可以接受多个客户端的连接
使用epoll实现服务点代码:
1 //epoll_server.c 只有一个进程也支持多客户端连接 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <arpa/inet.h> 9 #include <ctype.h> 10 #include <signal.h> 11 #include <sys/wait.h> 12 #include <errno.h> //for EINTR 13 #include <sys/epoll.h> 14 15 char server_ip[] = "127.0.0.1"; 16 int server_port = 8888; 17 18 int main(int argc, const char* argv[]){ 19 // 创建监听的套接字 20 int lfd = socket(AF_INET, SOCK_STREAM, 0); 21 if(lfd == -1){ //socket()失败则返回-1s 22 perror("socket error"); 23 exit(1); 24 } 25 26 // lfd 和本地的IP port绑定 27 struct sockaddr_in server; 28 memset(&server, 0, sizeof(server)); 29 server.sin_family = AF_INET; // 地址族协议 - ipv4 30 server.sin_port = htons(server_port); 31 //server.sin_addr.s_addr = htonl(INADDR_ANY); //htons(a)表示将在主机中的小端存储方式转换为网络中的大端存储方式 s表示shrot 32 inet_pton(AF_INET,server_ip,&server.sin_addr.s_addr); //使用自己指定的ip 33 int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server)); //同理htonl() l表示long 34 if(ret == -1){ 35 perror("bind error"); 36 exit(1); 37 } 38 39 // 设置监听 40 ret = listen(lfd, 36); //最多可监听的数目为36 41 printf("start accept......\n"); 42 43 struct sockaddr_in clientInfo; 44 socklen_t cli_len = sizeof(clientInfo); //sizeof(sockaddr_in)也是可以得 45 if(ret == -1){ 46 perror("listen error"); 47 exit(1); 48 } 49 50 //创建epoll树的根节点 51 int epfd = epoll_create(2000); //初始化树的大小为2000,超过2000也可以 52 //初始化epoll树 53 struct epoll_event ev; 54 ev.events = EPOLLIN; 55 ev.data.fd = lfd; 56 epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); //将ev挂在epoll树上 57 // 58 struct epoll_event all[2000]; 59 while(1){ 60 //使用epoll()通知内核做文件流的检测 61 int ret = epoll_wait(epfd,all,sizeof(all)/sizeof(all[0]),-1); //-1表示一直阻塞 62 63 //使用ret遍历all 64 for(int i=0;i<ret;++i){ 65 int fd = all[i].data.fd; 66 //判断是否有新连接 67 if(fd == lfd){ 68 //接收线连接 69 int cfd = accept(lfd,(struct sockaddr*)&clientInfo,&cli_len); //accept()不会发生阻塞 70 if(cfd == -1){ 71 perror("accept err"); 72 exit(1); 73 } 74 //打印新加入的客户端ip和port 75 char clientIP[64]; 76 int clientPort; 77 inet_ntop(AF_INET,&clientInfo.sin_addr.s_addr,clientIP,sizeof(clientIP)); 78 clientPort = ntohs(clientInfo.sin_port); 79 printf("new client IP:%s,port:%d\n",clientIP,clientPort); 80 81 //将cfd对应的信息挂到epoll树上,由于树上除根节点外,所有的节点的数据类型为struct epoll_event类型的,所以需要新建这个类型的结构体变量,然后再挂在树上 82 struct epoll_event temp; 83 temp.events = EPOLLIN; 84 temp.data.fd = cfd; 85 epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&temp); //将temp挂到epoll树上 86 } 87 else{ 88 //处理已经连接了的客户端发送过来的数据 89 if(! all[i].events & EPOLLIN){ //如果不是读事件 90 continue; //返回到for循环的地方重新开始循环 91 } 92 //读数据 93 char buf[1024] = {0}; //一定要初始化的,否则会显示异常,这里也起到了将上一次传送的数据清空的作用 94 int len = recv(fd,buf,sizeof(buf),0); 95 if(len == -1){ 96 perror("recv err"); 97 exit(1); 98 } 99 else if(len == 0){ 100 printf("客户端已经断开连接\n"); 101 close(fd); 102 //从树上删掉fd对应的结点 103 epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL); 104 } 105 else{ 106 //正常读数据情况 107 printf("recv buf:%s\n",buf); 108 send(fd,buf,len,0); // send(fd,buf,sizeof(buf)-1,0);也是可以得,将buf中的数据再次发出之后,buf中的数据是不会改变的 109 printf("after send(),strlen(buf)=%d\n",strlen(buf)); 110 buf[0] = '\0'; //清空buf数组中的数据 111 } 112 } 113 } 114 } 115 close(lfd); 116 return 0; 117 }
其中可以将83行代码修改为:
temp.events = EPOLLIN | EPOLLET; //将epoll设置为边沿触发模式
执行结果:
3)使用fcntl()将recv(fd,buf,sizeof(buf))中fd默认阻塞改变为非阻塞的方法及epoll的触发方式
使用fcntl()将recv(fd,buf,sizeof(buf))中fd默认阻塞改变为非阻塞的方法
1 /* 2 关于recv(int fd,char *buf,sizeof(buf)); 3 由于文件描述符fd默认的属性是阻塞的,即当fd对应的缓冲区中没有数据的时候,recv()阻塞在那里(执行不下去,没有返回值也不会向下执行);---同read(fd,buf,sizeof(buf)) 4 可以修改fd的属性为非阻塞的,那么当fd对应的缓冲区没有数据的时候,recv()也不会阻塞; 5 修改fd的属性为非阻塞的方法为: 6 int flag = fctl(fd,F_GETFL); //获取fd属性 7 flag |= O_NONBLOCK; //将非阻塞属性添加到flag中 8 fcntl(fd,F_SETFL,flag); //将fd的属性设置为新的flag属性 9 */
epoll的触发方式
1 /* 2 epoll三种工作模式 3 1、水平触发模式(默认)(LT) 4 只要fd对应的缓冲区有数据,则会一直读 5 2、边沿触发模式(ET)---为了提高epoll效率 6 客户端给服务端发一次数据,服务端epoll_wait()就返回一次, 7 不在乎是否读完客户端发送过来的数据,没有接受完的数据,会保留在缓冲区,等到下次客户端再次输入的时候,服务端先读上次没有读完的 8 解决方法,使用循环读,即 9 while(recv(fd,buf,sizeof(buf))){ ... } 10 但是最后一次读的时候,此时fd对应的缓冲区没有数据了,那么recv就会阻塞在那里,不会执行任何代码 11 解决方法是将fd的属性改变为非阻塞的状态。 12 13 3、边沿非阻塞 14 效率最高,方法如下: 15 //修改fd的属性为非阻塞的方法为: 16 int flag = fctl(fd,F_GETFL); //获取fd属性 17 flag |= O_NONBLOCK; //将非阻塞属性添加到flag中 18 fcntl(fd,F_SETFL,flag); //将fd的属性设置为新的flag属性 19 while((recv(fd,buf,sizeof(buf))) > 0){ 20 printf("recv buf:%s\n",buf); 21 } 22 */
epoll的两种工作方式(这里只介绍ET和LT模式):更新
1)epoll的事件指定为EPOLLIN的时候
1 /* 2 1、水平触发模式(LT) 3 当epoll_wait检测到fd上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通知此事件,直到此事件被处理。 4 5 2、边沿触发模式(ET) 6 当epoll_wait检测到fd上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。 7 处理该事件的方法就是循环读,以保证客户端发送过来的数据可以读完。 8 9 3、LT和ET的比较 10 可见ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此ET模式效率比LT模式高 11 12 4、注意 13 每个使用ET模式的文件描述符都应该是非阻塞的,如果ET模式下的文件描述符是阻塞的,由于在ET模式下需要一次读完缓冲区所有的数据(使用循环读),那么在最后一次读完之后,缓冲区就没有数据了,那么recv()函数会发生阻塞,等待缓冲区中有数据。 14 15 LT模式下使用阻塞或者是非阻塞的文件描述符都是可以的。如果是阻塞的文件描述符,recv()在读数据的时候可以不使用循环读,这样没有读完的数据可以下一次再读。 16 */
10、使用UDP实现客户端和服务端通信
UDP通信流程
1 /* 2 UDP通信流程 3 4 server端流程: 5 //创建套接字 6 int lfd = socket(AF_INET,SOCK_DGRAM,0); 7 //绑定ip、port绑定到lfd 8 bind() 9 //通信 10 recvfrom(); //接收数据(保存数据之后会把客户端的ip和端口保存下来) 11 sendto(); //发送数据(通过保存下来的数据向客户端发送数据) 12 //关闭套接字 13 close(fd); 14 15 client端流程: 16 //创建套接字 17 int lfd = socket(AF_INET,SOCK_DGRAM,0); 18 //通信 19 recvfrom(); //接收数据(通过ip和端口给服务器发送数据) 20 sendto(); //发送数据 21 //关闭套接字 22 close(fd); 23 */
相关函数
1 ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,struct sockaddr* srcaddr,socklen_t *addrlen); 2 sockfd:文件描述符 3 buf:接收数据缓冲区 4 len:buf的最大容量 5 flags:0 6 src_addr: 传出参数,保存客户端的ip和端口 7 addrlen: 传入传出参数,src_addr大小的地址 8 ssize_t sendto(int sockfd,const void* buf,size_t len,int flags,const struct sockaddr *dest_addr,socklen_t addrlen); 9 sockfd: 文件描述符
客户端代码实现
1 //udp_client.c 2 #include <stdio.h> //for fgets() 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> //for memset() 8 #include <arpa/inet.h> 9 #include <ctype.h> 10 #include <signal.h> 11 #include <sys/wait.h> 12 #include <errno.h> //for EINTR 13 #include <sys/select.h> 14 15 int main(){ 16 //创建套接字 17 int fd = socket(AF_INET,SOCK_DGRAM,0); 18 19 //创建一个server的ip和端口 20 struct sockaddr_in server; 21 memset(&server,0,sizeof(server)); 22 server.sin_family = AF_INET; 23 server.sin_port = htons(8888); 24 inet_pton(AF_INET,"127.0.0.1",&server.sin_addr.s_addr); 25 26 27 //通信 28 while(1){ 29 //数据的发送---发给server,需要知道server的ip和端口 30 char buf[1024] = {0}; 31 fgets(buf,sizeof(buf),stdin); //从终端输入到buf 32 senfto(fd,buf,sizeof(buf),(struct sockaddr*)&server,sizeof(server)); 33 34 //等待接收数据 35 recvfrom(fd,buf,sizeof(buf),0,NULL,NULL); //由于recvfrom()是对第5、6个参数写入,现在已经知道了服务端的ip和端口,故此时传入NULL即可 36 printf("recv buf:%s",buf); 37 } 38 close(fd); 39 return 0; 40 }
服务端代码实现
1 //usp_server.c 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> //for recvform()、sendto() 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <arpa/inet.h> 9 #include <ctype.h> 10 #include <signal.h> 11 #include <sys/wait.h> 12 #include <errno.h> //for EINTR 13 #include <sys/socket.h> //for recvform()、sendto()、socket() 14 #include <arpa/inet.h> 15 16 int main(){ 17 int lfd = socket(AF_INET,SOCK_DGRAM,0); //UDP通信对应SOCK_DGRAM 18 if(lfd == -1){ 19 perror("socket err"); 20 exit(1); 21 } 22 //lfd绑定ip和端口 23 struct sockaddr_in server; 24 server.sin_family = AF_INET; 25 server.sin_port = htons(8888); 26 server.sin_addr.s_addr = htons(INADDR_ANY); 27 int ret = bind(lfd,(struct sockaddr*)&server,sizeof(server)); 28 if(ret == -1){ 29 perror("bind err"); 30 exit(1); 31 } 32 33 //定义保存客户端ip和port的结构体变量 34 struct sockaddr_in clientInfo; 35 socklen_t clientInfoLen = sizeof(clientInfo); 36 37 //通信 38 while(1){ 39 char buf[1024] = {0}; 40 char clientIP[64]; 41 int clientPort; 42 int len = recvfrom(lfd,buf,sizeof(buf),(sockaddr*)&clientInfo,&clientInfoLen); 43 if(len == -1){ 44 perror("recvform err"); 45 exit(1); 46 } 47 else if(len == 0){ 48 printf("客户端已经断开连接\n"); 49 } 50 else{ 51 inet_pton(AF_INET,&clientInfo.sin_addr.s_addr,clientIP); 52 clientPort = ntohs(clientInfo.sin_port); 53 printf("recv form client IP:%s,port:%d,buf:%s",clientIP,clientPort,buf); 54 55 //给客户端回发数据 56 sendto(lfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&clientInfo,sizeof(clientInfo)); 57 } 58 } 59 }
上面的ercv()函数参数少些了一个,正确的写法应该是:
int len = recvfrom(lfd,buf,sizeof(buf),0,(sockaddr*)&clientInfo,&clientInfoLen);
测试结果:
11、广播和广播地址 mm211
基本概念
1 /* 2 udp通信---广播 3 server端主动发数据(只发数据) 4 client端被动的去接受数据 5 6 广播地址、固定端口 7 网关:起到ip地址解析的功能,假如ip地址1个ip地址2发送数据,那么整个流程为ip1--->ip1的网关--->ip2的网关--->ip2 8 9 广播地址:ip地址的最后一位是255即xxx.xxx.xxx.255表示在局域网内的广播地址 10 255.255.255.255在所有网段下的ip都可以接受的到这个网址服务端发送的数据 11 如果一个服务端使用了广播地址,那么在该网段下的所有ip地址都会收到服务端发送的数据 12 */
广播流程
1 /* 2 广播(只适用于局域网和UDP) 3 服务端 4 创建套接字-int fd = socket() 5 将ip和端口绑定到fd 6 发送数据 7 struct sockaddr_in client; 8 client.family = AF_INET; 9 client.port = htons(9898); 10 inet_pton(AF_INET,"xxx.xxx.xxx.255",&client.sin_addr.s_addr) 11 sendto(fd,buf,sizeof(buf),0,&client,sizeof(client)) 12 给服务器设置广播权限 13 setsockept(); 14 客户端 15 创建套接字-int fd = socket(); 16 绑定ip和端口,bind(); 17 接收数据--server发来的数据,不需要知道server端的ip和端口,因为在recvfrom()的时候自动将server端的ip和端口保存下来了 18 recvfrom(); 19 */
服务端代码
1 //broadcats_server.c 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> //for setsockopt() 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <arpa/inet.h> 9 #include <sys/socket.h> //for setsockopt()、sendto() 10 11 int main(int argc,const char* argv[]){ 12 //创建套接字 13 int fd = socket(AF_INET,SOCK_DGRAM,0); 14 if(fd == -1){ 15 perror("socket error"); 16 exit(1); 17 } 18 //绑定广播ip地址和端口 19 struct sockaddr_in server; 20 memset(&server,0,sizeof(server)); 21 server.sin_family = AF_INET; 22 server.sin_port = htons(8787); 23 server.sin_addr.s_addr = htonl(INADDR_ANY); 24 int ret = bind(fd,(struct sockaddr*)&server,sizeof(server)); 25 if(ret == -1){ 26 perror("bind error"); 27 exit(1); 28 }; 29 30 //初始化客户端的ip和端口 31 struct sockaddr_in client; 32 client.sin_family = AF_INET; 33 client.sin_port = htons(6767); //客户端要绑定的端口 34 inet_pton(AF_INET,"192.168.123.255",&client.sin_addr.s_addr); //使用广播地址给客户端发送数据,服务端会发送数据给123网段的所有电脑,关键就是看客户端有没有端口为6767接收数据的程序 35 36 //给服务端设置开放广播权限 37 int flag = 1; 38 setsockopt(fd,SOL_SOCKET,SO_BROADCAST,&flag,sizeof(flag)); 39 40 //通信 41 while(1){ 42 //一直给客户端发送数据 43 static int num = 0; 44 char buf[1024] = {0}; 45 sprintf(buf,"hello usp==%d\n",num++); //向buf中写入hello usp==%d 46 int len = sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client)); 47 if(len == -1){ 48 perror("send err"); 49 exit(1); 50 } 51 printf("server send buf:%s\n",buf); 52 53 sleep(1); 54 } 55 56 close(fd); 57 return 0; 58 }
客户端
1 //broadcats_client.c 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <arpa/inet.h> 9 10 11 int main(int argc,char* argv[]){ 12 //创建套接字 13 int fd = socket(AF_INET,SOCK_DGRAM,0); 14 if(fd == -1){ 15 perror("socket err"); 16 exit(1); 17 } 18 //绑定ip和端口 19 struct sockaddr_in client; 20 client.sin_family = AF_INET; 21 client.sin_port = htons(6767); //server端发送数据到6767端口也就是发送到这个客户端下 22 inet_pton(AF_INET,"0.0.0.0",&client.sin_addr.s_addr); //0.0.0.0表示当前主机ip,会自动适配上本机ip 23 int ret = bind(fd,(struct sockaddr*)&client,sizeof(client)); 24 if(ret == -1){ 25 perror("bind err"); 26 exit(1); 27 } 28 29 //接收数据 30 while(1){ 31 char buf[1024] = {0}; 32 int len = recvfrom(fd,buf,sizeof(buf),0,NULL,NULL); //NULL表示不需要保存服务端的ip和端口 33 if(len == -1){ 34 perror("recvfrom err"); 35 exit(1); 36 } 37 printf("recv buf:%s\n",buf); 38 } 39 40 close(fd); 41 42 return 0; 43 }
测试结果:
12、组播、广播和组播的区别mm212
相关概念
1 组播(multicast)---适用于局域网和广域网(Internet) 2 服务端使用组播地址:发送到客户端端口上,添加组播权限 3 服务端:绑定端口,使用setsockopt()加入到组播地址 4 5 组播地址: 6 224.0.0.0~224.0.0.255 7 预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其他地址供路由协议使用 8 224.0.1.0~224.0.1.255 9 公用组播地址,全网范围内可用----支付宝等 10 239.0.0.0~239.255.255.255 11 本地管理的本地地址,仅在特定的本地范围内有效 12 13 14 服务端流程---只发送数据 15 16 struct ip_mreqn{ 17 struct in_addr imr_multiaddr; //本组播的ip地址 18 struct in_addr imr_address; //本地某一个网段设备接口的IP地址 19 int imr_ifindex; //网卡编号 20 21 }; 22 23 struct in_addr{ 24 in_addr_t s_addr; 25 }; 26 27 网卡编号即硬件地址 使用ifconfig查看,第一列为网卡名字 28 #include <net/if.h> 29 unisgned int if_nametoindex(const char *ifname); //通过网卡名字获取硬件地址
广播和组播的区别在于:只要客户端在服务端的网段下,广播的客户端不可以选择是否去接收服务端发送的数据;但是组播就可以通过设置是否加入服务端设置的组播地址来决定是否接受服务端发送的数据。
组播服务端代码
13、TCP和UDP的区别
1 /* 2 TCP与UDP区别总结: 3 1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。在编程上UDP不需要listen()和accept() 4 2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保 证可靠交付 5 3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的 6 UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等) 7 4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信 8 5、TCP首部开销20字节;UDP的首部开销小,只有8个字节 9 6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道 10 */
2、UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket(); 2、设置socket属性,用函数setsockopt();* 可选 3、绑定IP地址、端口等信息到socket上,用函数bind(); 4、循环接收数据,用函数recvfrom(); 5、关闭网络连接;
3、UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket(); 2、设置socket属性,用函数setsockopt();* 可选 3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选 4、设置对方的IP地址和端口等属性; 5、发送数据,用函数sendto(); 6、关闭网络连接;
三、常用函数介绍
1、socket()---用于服务端和客户端
函数头文件及函数原型:
1 #include <sys/types.h> /* See NOTES */ 2 #include <sys/socket.h> 3 int socket(int domain, int type, int protocol);
参数及返回值说明:
1 domain: 2 AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址 3 AF_INET6 与上面类似,不过是来用IPv6的地址 4 AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用 5 type: 6 SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。 7 SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。 8 SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。 9 SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议) 10 SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序 11 protocol: 12 传0 表示使用默认协议。 13 返回值: 14 成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
socket()说明:
1 socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
2、bind()---用于服务端
函数作用:是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
函数头文件及函数原型
1 #include <sys/types.h> /* See NOTES */ 2 #include <sys/socket.h> 3 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数及返回值说明:
1 sockfd: 2 socket文件描述符 3 addr: 4 构造出IP地址加端口号 5 addrlen: 6 sizeof(addr)长度 7 返回值: 8 成功返回0,失败返回-1, 设置errno
bind()第二个参数sockaddr是一个结构体,原型如下:
1 struct sockaddr { 2 sa_family_t sin_family;//地址族 3 char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息 4 };
由于sockaddr结构体中的成员sa_data将目标地址(目标IP地址)和端口混在一起了,所以不使用这个结构体,而是要另外一个结构体socketaddr_in,最后将sockaddr_in强制转换一下即可,sockaddr_in结构体原型如下:
1 struct sockaddr_in{ 2 sa_family_t sin_family; //地址族(Address Family) 3 uint16_t sin_port; //16位TCP/UDP端口号 4 struct in_addr sin_addr; //32位ip地址 5 char sin_zero[8]; //不使用 6 }; 7 8 struct in_addr{ 9 In_addr_t s_addr; //32位ipv4地址 10 };
由于在TCP/UDP/IP协议中规定使用大端存储方式(网络自己序),而在我们的电脑中使用的是小端模式,因此需要转换函数,做存储模式的转换:
1 #include <arpa/inet.h> 2 3 //将主机字节序转换为网络字节序 4 unit32_t htonl (unit32_t hostlong); 5 unit16_t htons (unit16_t hostshort); 6 //将网络字节序转换为主机字节序 7 unit32_t ntohl (unit32_t netlong); 8 unit16_t ntohs (unit16_t netshort); 9 10 说明:h -----host;n----network ;s------short;l----long。
使用方法如下:
1 struct sockaddr_in server; 2 memset(&server, 0, sizeof(server)); //先将结构体清零 3 server.sin_family = AF_INET; // 地址类型 - ipv4 4 server.sin_port = htons(8888); 5 server.sin_addr.s_addr = htonl(INADDR_ANY); //htons(a)表示将在主机中的小端存储方式转换为网络中的大端存储方式 s表示shrot 6 int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server)); //同理htonl() l表示long
INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址
3、listen()---用于服务端
头文件及函数原型
1 #include <sys/types.h> /* See NOTES */ 2 #include <sys/socket.h> 3 int listen(int sockfd, int backlog);
参数说明
1 sockfd: 2 socket文件描述符 3 backlog: 4 排队建立3次握手队列和刚刚建立3次握手队列的链接的最大数 5 或者是可以创建连接的最大数
返回值
listen()成功返回0,失败返回-1
listen()函数说明
1 /* 2 典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。 3 */
4、accetp()---用于服务端
头文件即函数原型
1 #include <sys/types.h> /* See NOTES */ 2 #include <sys/socket.h> 3 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明及函数返回值
sockdf: socket文件描述符 addr: 传出参数,返回链接客户端地址信息,含IP地址和端口号 addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小 返回值: 成功返回一个新的socket文件描述符,用于和客户端通信,通过这个读新的文件描述符就可以得到客户端发来的信息,失败返回-1,设置errno
函数说明
1 /* 2 三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。 3 */
使用方法
1 struct sockaddr_in client; 2 socklen_t len = sizeof(client); 3 int cfd = accept(lfd, (struct sockaddr*)&client, &len); //如果没有客户端请求连接,则阻塞在这里,其中client为传出参数,包含了客户端的ip和端口号 4 if(cfd == -1) 5 { 6 perror("accept error"); 7 exit(1); 8 }
accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,通过cfd读取客户端发来的信息的方法:
1 char buf[1024] = {0}; 2 int len = read(cfd, buf, sizeof(buf)); 3 //len=-1读失败 4 //len=0客户端关闭 5 //len>0读成功
5、connect()----用于客户端
头文件及函数原型
1 #include <sys/types.h> /* See NOTES */ 2 #include <sys/socket.h> 3 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数及函数返回值说明
1 sockdf: 2 socket文件描述符 3 addr: 4 传入参数,指定服务器端地址信息,含IP地址和端口号 5 addrlen: 6 传入参数,传入sizeof(addr)大小 7 返回值: 8 成功返回0,失败返回-1,设置errno
函数说明
1 /* 2 客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。 3 */
6、fcntl()函数---获得并改变文件描述符的属性
头文件及函数原型
1 #include <unistd.h> 2 #include <fcntl.h> 3 4 函数原型: 5 int fcntl(int fd, int cmd); //获得文件描述符fd属性,cmd选择为F_GETFL 6 int fcntl(int fd, int cmd, long arg); //通过cmd命令设置文件描述符fd的属性为arg,cmd选择为SETFL 7 int fcntl(int fd, int cmd, struct flock *lock);
cmd可选的选项有:
7、read()或recv()函数返回EAGAIN错误
这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读,此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
举例:设置文件描述符为非阻塞状态后,使用recv()去读fd对应的缓冲区时,如果是连续读,最后一次读时候没有数据可读,此时recv()返回EAGAIN错误,错误示例:
1 //将cfd对应的信息挂到epoll树上,由于树上除根节点外,所有的节点的数据类型为struct epoll_event类型的,所以需要新建这个类型的结构体变量,然后再挂在树上 2 struct epoll_event temp; 3 temp.events = EPOLLIN | EPOLLET; //设置边沿触发模式 4 int flag fctl(cfd,F_GETFL); //获取cfd原来属性 5 flga |= O_NONBLOCK; //将非阻塞属性添加到flag 6 fcntl(cfd,F_SETFL,flag); //将新属性(非阻塞)添加到cfd,这样在recv(cfd,buf,sizeof(buf))的时候如果fd对应的缓冲区中没有数据就不会发生阻塞了 7 8 9 //读数据 10 char buf[1024] = {0}; //一定要初始化的,否则会显示异常,这里也起到了将上一次传送的数据清空的作用 11 int len; 12 while((len=recv(fd,buf,sizeof(buf),0)) > 0){ 13 write(STDOUT_FILENO,buf,len); //打印到终端 14 send(fd,buf,sizeof(buf)-1); //回发过去 15 } 16 if(len == -1){ 17 perror("recv err"); 18 exit(1); 19 20 } 21 else if(len == 0){ 22 printf("客户端已经断开连接\n"); 23 close(fd); 24 //从树上删掉fd对应的结点 25 epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL); 26 }
此时当读完fd中的数据,再读一次的时候,此时recv()会返回-1,并将erro设置为EAGAIN,上面的程序是直接退出,那么我们可以根据EAGAIN这个标志解决上面程序退出的问题:
1 //将cfd对应的信息挂到epoll树上,由于树上除根节点外,所有的节点的数据类型为struct epoll_event类型的,所以需要新建这个类型的结构体变量,然后再挂在树上 2 struct epoll_event temp; 3 temp.events = EPOLLIN | EPOLLET; //设置边沿触发模式 4 int flag fctl(fd,F_GETFL); //获取cfd原来属性 5 flag |= O_NONBLOCK; //将非阻塞属性添加到flag 6 fcntl(fd,F_SETFL,flag); //将新属性(非阻塞)添加到cfd,这样在recv(cfd,buf,sizeof(buf))的时候如果fd对应的缓冲区中没有数据就不会发生阻塞了 7 8 9 //读数据 10 char buf[1024] = {0}; //一定要初始化的,否则会显示异常,这里也起到了将上一次传送的数据清空的作用 11 int len; 12 while((len=recv(fd,buf,sizeof(buf),0)) > 0){ 13 write(STDOUT_FILENO,buf,len); //打印到终端 14 send(fd,buf,sizeof(buf)-1); //回发过去 15 } 16 if(len == -1){ 17 if(errno == EAGAIN){ //解决非阻塞状态下,最后一次recv()返回-1并设置error为EAGAIN的情况 18 printf("缓冲区数据已经读完\n"); 19 } 20 else{ 21 perror("recv err"); 22 exit(1); 23 } 24 } 25 else if(len == 0){ 26 printf("客户端已经断开连接\n"); 27 close(fd); 28 //从树上删掉fd对应的结点 29 epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL); 30 }
8、inet_pton()----将点分十进制转换为网络传输用的数据个
函数原型和头文件
1 /* 2 #include <arpa/inet.h> 3 int inet_pton(int family, const char *strptr, void *addrptr); //将点分十进制的strptr地址转化为用于网络传输的数值格式addrptr 4 返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1 5 */
family参数既可以是AF_INET(ipv4)也可以是AF_INET6(ipv6).如果,以不被支持的地址族作为family参数,这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT.
inet_pton()尝试转换由strptr指针所指向的字符串,并通过addrptr指针存放二进制结果,若成功则返回值为1,否则如果所指定的family而言输入字符串不是有效的表达式格式,那么返回值为0.
9、inet_ntop()----将网络传输用的数据个数转换为点分十进制格式
函数原型和头文件
1 /* 2 #include <arpa/inet.h> 3 const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //将网络数值格式的addrptr转化为点分十进制的strptr 4 返回值:若成功则为指向结构的指针,若出错则为NULL 5 */
inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达式(strptr)。inet_ntop函数的strptr参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。如果len太小,不足以容纳表达式结果,那么返回一个空指针,并置为errno为ENOSPC。
10、send()、write()函数返回值
send():函数原型
int send( SOCKET s, const char FAR *buf, int len, int flags );
该函数的第一个参数指定发送端套接字描述符;
第二个参数指明一个存放应用程序要发送数据的缓冲区;
第三个参数指明实际要发送的数据的字节数;
第四个参数一般置0。
返回值:发送成功则返回试剂发送的字节数,否则返回SOCKET_ERROR
write()函数原型
size_t write (int fd,const void * buf,size_t count);
write()会把指针buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。
如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。
四、TCP三次握手及四次挥手
1、三次握手
标志位:
SYN:请求建立连接,SYN占一个字节
ACK:应答
FIN: 断开连接
1 第一次握手 2 客户端:携带一个标志位SYN+随机产生一个32位信号a,可以携带数据 3 服务端: 检测SYN的值是否为1,若是,则第一次握手成功 4 第二次握手: 5 服务端: 6 发送一个ACK标志位+确认信号1(客户端发送过来的随机序号加1,即确认信号为a+1) 7 发起一个连接请求: SYN + 一个随机信号b 8 客户端: 9 检测ACK标志位是否为1,并检测32位的确认信号是否为a+1,如果两个条件都满足,则第二次握手成功 10 第三次握手: 11 客户端:发送一个确认数据包:ACK+确认信号(服务端发送过来的随机序号加1,即确认信号为b+1) 12 服务端:检测ACK是否为1,且确认信号是否为b+1
第一次握手的时候客户端可以带一个数据,假如发送的数据包为:SYN+1000(10),则客户端返回的数据包为:ACK+1011, SYN+2000(12),
其中括号内为携带的参数,参数为携带的数据包的大小,一般在三次握手中参数为0
三次握手对应于socket中:
客户端的connect()、服务端对额accept()
TCP数据包结构:
TCP通信流程:
为什么是三次握手而不是二次
为了实现可靠传输,发送方和接收方始终需要同步( SYNchronize )序号。 需要注意的是, 序号并不是从 0 开始的, 而是由发送方随机选择的初始序列号 ( Initial Sequence Number, ISN )开始 。 由于 TCP 是一个双向通信协议, 通信双方都有能力发送信息, 并接收响应。 因此, 通信双方都需要随机产生一个初始的序列号, 并且把这个起始值告诉对方。
于是, 这个过程就变成了下面这样。
2、四次挥手
- 第一次:客户端请求断开FIN + 序列号u(确认序号u是服务端最后一次发送给客户端ACK所携带的32位序号) + ACK + 序列号u2(确认序号u2是客户端最后一次发送给服务端ACK所携带的32位序号)
- 第二次:服务器确认客户端的发送的FIN是否为1,并发送ACK + 序列号u+1 ;之后客户端会去确认ACK是否为1,并确认序列号是否为u+1
- 第三次:服务器请求断开FIN + w ,ACK +( u+1) ;之后客户端会去确认FIN是否为1
- 第四次:客户端确认服务器的断开ACK,发送ACK + (w+1)
对应于程序中的 服务端: close(fd); 客户端:close(fd);
3、tcp状态转换
tcp状态转换文字描述版
1 /* 2 1、tcp状态转换 3 首先发送数据请求的一端状态会首先改变,下面以客户端首先发送状态请求: 4 (三次握手期间客户端和服务端状态变化:) 5 客户端初始化为CLOSED状态-->客户端给服务端发送SYN+a请求(此时客户端状态改变为SYN_SEND)--->服务端发送SYN+b、ACK+(a+1)(此时服务端状态变为SYD_RECV状态) 6 --->客户端确认服务单发送的状态,并发送ACK+(b+1)给服务端(此时客户端状态改变为ESTABLISHED)--->服务端收到了客户端发送的ACK+(b+1)(此时服务端状态改变为ESTABLISHED) 7 (在通信阶段,服务端和客户端的状态都为ESTABLISHED,且不改变) 8 9 四次挥手阶段,假如客户端首先发送断开连接请求(注x是服务端最后一次发送ACK给客户端时携带的数据,y为客户端最后一次发送给服务端时ACK所携带的值): 10 客户端发送FIN+x、ACK+y(此时客户端状态改变为FIN_WAIT_1)--->服务端发送ACK+(x+1)给客户端(此时服务端状态变为CLOSE_WAIT)--->客户端接收到服务端发送的ACK后状态改变为FIN_WAIT_2 11 --->服务端发送FIN+(y)给客户端(此时服务端状态变为LAST_ACK)--->客户端发送ACK+(x+1)给服务端(此时客户端状态改变为TIME_WAIT)--->客户端状态变为CLOSED状态 12 */
小知识点
1 2、能捕捉到的状态:ESTABLISHED 2 3 3、2MSL 4 客户端(主动断开的一方)在TIME_WAIT状态会等待2MLS(2*30s)再装换为CLOSED状态 5 服务端(被动断开的一方)不需要等待 6 客户端等待2MSL的目的是为了保证服务端能够接受到客户端最后一次发送的ACK+(x+1) 7 2MSL是为了保证四次挥手的最后一次挥手能够正常进行
半关闭状态
1 4、半关闭状态 2 有一端断开连接,但是另外一端没有断开 3 A给B发送了FIN(A调用了close()),但是B没有给A发送FIN(B没有调用close()) 4 A不能给B发送数据,但是A可以接受B发送的数据,B可以给A发送数据
4、文件描述符赋值函数dup2()及netstat命令
文件描述符复制函数
1 5、文件描述符复制函数 2 dup2(old,new); //将文件描述符old复制给new 3 例如: 4 dup2(sfd,fd); //将sfd复制给fd 5 close(fd); //只是fd不能使用了,但是sfd还是可以使用的
netstat命令:查看网络相关状态信息
1 6、netstat命令:查看网络相关状态信息 2 参数: 3 -a 显示所有选项,默认不显示处于监听(listen())状态的进程 4 -p 显示建立连接的程序名 5 -n 拒绝显示别名,能显示数字的全部转化为数字 6 -t 仅显示tcp相关选项 7 -u 仅显示udp相关选项 8 -l 仅列出处于监听(listen())状态的进程 9 10 netstat -apn | grep xxx 常用使用方法,xxx为程序名字 11 netstat -apn | grep 8888 8888为端口号 12 前边是服务器信心 后面是客户端信息
5、“bind error: Address already in use”错误解决方法
1 /* 2 服务端主动断开后,一分钟内再次运行服务端,会报错如下: 3 bind error: Address already in use 4 这是由于服务端主动断开,然后服务端会有一个2MSL的等待时长,2MSL后即可正常运行 5 使用程序解决方法:使用端口复用函数: 6 #include <sys/sockte.h> 7 int setsockopt(int sockfd,int level,int opname,void* optval,socklen_t optlen) 8 参数选择: 9 sockfd传入监听的套接字,因为监听的套接字才和端口有关系(服务端有监听和通信的两个套接字) 10 level 选择为SOL_SOCKET 11 opname 选择为SO_REUSEPORT或者SO_REUSEADDR都可以 12 optval 定义一个int型变量a,并给a赋值为1,表示这个属性被启用 13 optlen 选择为a的地址大小即可 14 在哪儿调用? 15 在绑定(bind())之前设置这个端口是否可以被复用 16 17 例如: 18 设置端口复用 19 int flag=1; 20 setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,flag,&flag,sizeof(flag)); 21 bind(lfd,(struct sockaddr*)&server,sizeof(server)); //绑定 22 */
6、TCP传输协议如何保证要传输的数据的可靠性?
TCP协议保证数据传输可靠性的方式主要有:
- 校验和
- 序列号
- 确认应答
- 超时重传
- 连接管理
- 流量控制
- 拥塞控制
校验和
计算方式:在数据传输的过程中,将发送的数据段都当做一个16位的整数。将这些整数加起来。并且前面的进位不能丢弃,补在后面,最后取反,得到校验和。
发送方:在发送数据之前计算检验和,并进行校验和的填充。
接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方的进行比对。
注意:如果接收方比对校验和与发送方不一致,那么数据一定传输有误。但是如果接收方比对校验和与发送方一致,数据不一定传输成功。
序列号和确认应答(ACK)
序列号:TCP传输时将每个字节的数据都进行了编号,这就是序列号。
确认应答:TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。
序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。这也是TCP传输可靠性的保证之一。
超时重传
在进行TCP传输时,由于确认应答与序列号机制,也就是说发送方发送一部分数据后,都会等待接收方发送的ACK报文,并解析ACK报文,判断数据是否传输成功。如果发送方发送完数据后,迟迟没有等到接收方的ACK报文,这该怎么办呢?而没有收到ACK报文的原因可能是什么呢?
首先,发送方没有介绍到响应的ACK报文原因可能有两点:
- 数据在传输过程中由于网络原因等直接全体丢包,接收方根本没有接收到。
- 接收方接收到了响应的数据,但是发送的ACK报文响应却由于网络原因丢包了。
TCP在解决这个问题的时候引入了一个新的机制,叫做超时重传机制。简单理解就是发送方在发送完数据后等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送。如果是刚才第一个原因,接收方收到二次重发的数据后,便进行ACK应答。如果是第二个原因,接收方发现接收的数据已存在(判断存在的根据就是序列号,所以上面说序列号还有去除重复数据的作用),那么直接丢弃,仍旧发送ACK应答。
那么发送方发送完毕后等待的时间是多少呢?如果这个等待的时间过长,那么会影响TCP传输的整体效率,如果等待时间过短,又会导致频繁的发送重复的包。如何权衡?
由于TCP传输时保证能够在任何环境下都有一个高性能的通信,因此这个最大超时时间(也就是等待的时间)是动态计算的。
连接管理
即三次握手、四次挥手
流量控制
接收端在接收到数据后,对其进行处理。如果发送端的发送速度太快,导致接收端的结束缓冲区很快的填充满了。此时如果发送端仍旧发送数据,那么接下来发送的数据都会丢包,继而导致丢包的一系列连锁反应,超时重传呀什么的。而TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。
在TCP协议的报头信息当中,有一个16位字段的窗口大小。在介绍这个窗口大小时我们知道,窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。
注:16位的窗口大小最大能表示65535个字节(64K),但是TCP的窗口大小最大并不是64K。在TCP首部中40个字节的选项中还包含了一个窗口扩大因子M,实际的窗口大小就是16为窗口字段的值左移M位。每移一位,扩大两倍。
拥塞控制
TCP传输的过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题。网络可能在开始的时候就很拥堵,如果给网络中在扔出大量数据,那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包,就对大量的超时重传,严重影响传输。
所以TCP引入了慢启动的机制,在开始发送数据时,先发送少量的数据探路。探清当前的网络状态如何,再决定多大的速度进行传输。这时候就引入一个叫做拥塞窗口的概念。发送刚开始定义拥塞窗口为 1,每次收到ACK应答,拥塞窗口加 1。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取较小的值作为实际发送的窗口。
拥塞窗口的增长是指数级别的。慢启动的机制只是说明在开始的时候发送的少,发送的慢,但是增长的速度是非常快的。为了控制拥塞窗口的增长,不能使拥塞窗口单纯的加倍,设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。在慢启动开始的时候,慢启动的阈值等于窗口的最大值,一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为 1。
五、Libevent库
1、Libevent库的安装
(1)在https://libevent.org/官网上将安装包下载下来,然后拖到虚拟机中,我是拖到了/home下的一个新建文件夹中
(2)、解压
(3)ls 查看是否存在configure文件,绿色的很好找,然后执行这个文件,目的是检测当前环境是否支持安装,并生成一个makefile,然后根绝这个makefile使用make执行这个makefile
如果需要自己指定源码的安装路径的话,可以使用 ./configure prefix==/usr/xxx 注意:/usr/xxx就是自己指定的源码安装路径
(4)执行刚刚生成的makefile,目的是编译源代码,生成一些动态、静态库、可执行程序
(5)将上一步生成的静态库、动态库、可执行程序拷贝到对应的目录下,如果目录不存在则创建该目录
默认的拷贝目录为 /usr/local,如果是头文件则放到/usr/local/include、如果是二进制文件则放到/usr/local/bin、如果是库文件则放到/usr/local/lib
(6)以上即安装完毕,接下来要验证是否安装成功,在原安装目录下,cd到sample
再开启一个客户端,使用n吃命令,nc命令数netcat的简称,使用nc可以监听某个端口,也可以往其他服务器端口发送数据
使用 nc 127.0.0.1 9995也是可以的
2、使用Libevent库读写有名管道(fifo文件)
(1)Libevent常用的函数及使用步骤
相关函数及使用的步骤
1 /* 2 相关函数 3 1、创建事件框架 4 struct event_base* event_base_new(void); //创建一个事件处理框架,在event_base结构体中封装了select poll epoll 5 2、创建事件(没有缓冲区) 6 typedef voi(*event_callback_in)(evutil_socket_t fd,short t,void* arg); 7 struct event* event_new( 8 sturct event_base *base, 9 evutil_socket_t fd, //文件描述符 相当于int类型 10 short what, //事件 11 event_callback_in cd, //事件发生后的执行的动作,是一个回调函数 12 void *arg; //指向回调函数cd 13 ); 14 此处回调函数作为一个形参,当event_new()的另外一个形参what条件满足的时候那么就调用这个回调函数 15 参数what对应的宏定义: 16 #define EV_READ 0x02; 17 #define EV_WRITE 0x04 18 #define EV_SIGNAL 0x08 19 #define EV_PERSIST 0x10 //持续触发事件,如果没有设置持续监测,那么在event_base_dispatch()中就只监测一次what对应的事件 20 21 3、添加事件,将非未决事件改变为未决事件;非未决事件是指该事件还没有资格被处理,未决事件的意思是该事件已经有了被处理的资格但是还没有被处理 22 int event_add( 23 struct event *ev; //将ev中的what对应的事件添加到ev中base事件框架中 24 const struct timeval *tv; 25 ); 26 tv参数的设置方法: 27 NULL:事件被触发,对应的回调函数被调用 28 tv等于一个数,表示在该时间到达后,回调函数会被强制的调用一次(即使事件没有被触发) 29 event_add()函数调用成功,返回0,失败返回-1 30 31 4、开始时间循环 32 int event_base_dispatch(struct event_base *base); 33 34 5、资源的释放 35 void event_free(struct event *event); 36 event_base_free(struct event_base* base); 37 38 */
(2)使用Libevent读写fifo文件的实现
1 //使用event读fifo管道(有名管道) 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <event2/event.h> 10 11 //事件对应的回调函数 12 void read_cd(evutil_socket_t fd,short what,void* arg){ 13 //读管道 14 char buf[1024]={0}; 15 int len=read(fd,buf,sizeof(buf)); 16 printf("read data:%s,length:%d\n",buf,len); 17 printf("read event:%s\n",what&EV_READ ? "Yes":"No"); 18 } 19 20 int main(int argc,char* argv[]){ 21 unlink("myfifo"); //如果存在myfifo文件则删除 22 //创建有名管道文件,0664为该文件的权限 23 mkfifo("myfifo",0664); 24 //open file O_NONBLOCK的目的是便于调试 25 int fd=open("myfifo",O_RDONLY | O_NONBLOCK); 26 if(fd == -1){ 27 perror("open error"); 28 exit(1); 29 } 30 31 //创建事件处理框架 32 struct event_base* base=NULL; 33 base=event_base_new(); 34 35 //创建事件 36 struct event* ev; 37 ev=event_new(base,fd,EV_READ | EV_PERSIST,read_cd,NULL); //read_cd是一个回调函数 38 39 //把事件ev添加到事件处理框架base中 40 event_add(ev,NULL); //NULL表示一直等待事件发生,发生了则执行回到函数 41 42 //开始事件循环 43 event_base_dispatch(base); 44 45 //释放资源 46 event_free(ev); 47 event_base_free(base); 48 close(fd); 49 50 return 0; 51 }
1 //使用event写fifo管道(有名管道) 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <event2/event.h> 10 11 //事件对应的回调函数 12 void write_cd(evutil_socket_t fd,short what,void* arg){ 13 //写管道 14 static int num=0; 15 char buf[1024]={0}; 16 sprintf(buf,"hello world:%d\n",num++); 17 write(fd,buf,sizeof(buf)+1); 18 sleep(1); //不能写的太快了 19 } 20 21 int main(int argc,char* argv[]){ 22 //open file O_NONBLOCK的目的是便于调试 23 int fd=open("myfifo",O_WRONLY | O_NONBLOCK); 24 if(fd == -1){ 25 perror("open error"); 26 exit(1); 27 } 28 29 //创建事件处理框架 30 struct event_base* base=NULL; 31 base=event_base_new(); 32 33 //创建事件 34 struct event* ev; 35 ev=event_new(base,fd,EV_WRITE|EV_PERSIST,write_cd,NULL); //read_cd是一个回调函数,检测写缓冲区是否可写,且持续写,即一直在调用回调函数write_cd() 36 37 //把事件ev添加到事件处理框架base中 38 event_add(ev,NULL); //NULL表示一直等待事件发生,发生了则执行回到函数 39 40 //开始事件循环 41 event_base_dispatch(base); 42 43 //释放资源 44 event_free(ev); 45 event_base_free(base); 46 close(fd); 47 48 return 0; 49 }
由于在libevent_write_fifo.c中事件的what属性设置为了EV_PERSIST,故在写事件对应的回调函数中没有使用while(1)循环写,也可以实现循环写的功能
上面程序读到的不太对,修改如下
1 //使用event读fifo管道(有名管道) 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <event2/event.h> 10 11 //事件对应的回调函数 12 void read_cd(evutil_socket_t fd,short what,void* arg){ 13 //读管道 14 char buf[1024]={0}; 15 int len=read(fd,buf,sizeof(buf)); 16 printf("read data:%s,length:%d\n",buf,len); 17 printf("read event:%s\n",what&EV_READ ? "Yes":"No"); 18 sleep(1); 19 } 20 21 int main(int argc,char* argv[]){ 22 unlink("myfifo"); //如果存在myfifo文件则删除 23 //创建有名管道文件,0664为该文件的权限 24 mkfifo("myfifo",0664); 25 //open file O_NONBLOCK的目的是便于调试 26 int fd=open("myfifo",O_RDONLY | O_NONBLOCK); 27 if(fd == -1){ 28 perror("open error"); 29 exit(1); 30 } 31 32 //创建事件处理框架 33 struct event_base* base=NULL; 34 base=event_base_new(); 35 36 //创建事件 37 struct event* ev; 38 ev=event_new(base,fd,EV_READ | EV_PERSIST,read_cd,NULL); //read_cd是一个回调函数 39 40 //把事件ev添加到事件处理框架base中 41 event_add(ev,NULL); //NULL表示一直等待事件发生,发生了则执行回到函数 42 43 //开始事件循环 44 event_base_dispatch(base); 45 46 //释放资源 47 event_free(ev); 48 event_base_free(base); 49 close(fd); 50 51 return 0; 52 }
1 //使用event写fifo管道(有名管道) 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <event2/event.h> 10 11 //事件对应的回调函数 12 void write_cd(evutil_socket_t fd,short what,void* arg){ 13 //写管道 14 static int num=0; 15 char buf[1024]={0}; 16 sprintf(buf,"hello world:%d",num++); 17 write(fd,buf,sizeof(buf)-1); //去掉buf中的换行符 18 sleep(1); //不能写的太快了 19 } 20 21 int main(int argc,char* argv[]){ 22 //open file O_NONBLOCK的目的是便于调试 23 int fd=open("myfifo",O_WRONLY | O_NONBLOCK); 24 if(fd == -1){ 25 perror("open error"); 26 exit(1); 27 } 28 29 //创建事件处理框架 30 struct event_base* base=NULL; 31 base=event_base_new(); 32 33 //创建事件 34 struct event* ev; 35 ev=event_new(base,fd,EV_WRITE|EV_PERSIST,write_cd,NULL); //read_cd是一个回调函数,检测写缓冲区是否可写,且持续写,即一直在调用回调函数write_cd() 36 37 //把事件ev添加到事件处理框架base中 38 event_add(ev,NULL); //NULL表示一直等待事件发生,发生了则执行回到函数 39 40 //开始事件循环 41 event_base_dispatch(base); 42 43 //释放资源 44 event_free(ev); 45 event_base_free(base); 46 close(fd); 47 48 return 0; 49 }
主要是将libevent_fifo_write.c中的
原sprintf()函数中:
1 sprintf(buf,"hello world:%d\n",num++);
修改为:
1 sprintf(buf,"hello world:%d",num++);
原write()函数中:
1 write(fd,buf,sizeof(buf)+1); //去掉buf中的换行符
修改为
1 write(fd,buf,sizeof(buf)-1); //去掉buf中的换行符
sizeof(buf)-1是由于sizeof()遇到空字符(\0)结束统计,同时会把空字符也统计上
1 string s = "123"; 2 cout<<sizeof("123")<<endl; //返回 4,包含了最后的空字符 3 cout<<strlen("123")<<endl; //返回 3 4 cout<<s.size()<<endl; //返回 3
3、使用libevent创建服务端
创建步骤:
一、创建事件处理框架---使用event_base_new()函数; 二、创建sockaddr_in serv结构体对象serv,用来保存服务端IP和端口; 三、创建监听的套接字、绑定、监听、接受新连接,使用一个函数evconnlistener_new_bind()函数实现 有新连接的时候,evconnlistener_new_bind()的形参之一listen_cb回调函数被调用; (1)listen_cb回调函数连接完成后对应的通信操作,创建步骤如下: 1)使用bufferevent_socket_new()创建带有读写缓冲区的事件bev(bev是该函数的返回值); 2)使用bufferevent_setcb()给bev的读写缓冲区分别设置回调函数read_cb,write_cb,event_cb,分别是读回调、写回调、事件回调; 3)由于bev中默认读是失能的,所以这里要使用bufferevent_enable()使能bev中的读缓冲区;写缓冲区默认是使能的; 4)接下来是分别写read_cb,write_cb,event_cb (2)在读回调read_cb函数中调用bufferevent_read()来读取bev中读缓冲区中的数据; (3)在写回调write_cb函数中调用bufferevent_write()向写bev中的写缓冲区写数据然后自动发送给客户端,最后会自动调用write_cb()
相关函数
/* 套接字通信要使用带缓冲区的事件---Bufferevent 1、头文件#include <event2/bufferevent.h> 2、bufferevent带缓冲区的事件中自带了一个读缓冲区和一个写缓冲区; (1)当读缓冲区中有数据的时候,读缓冲区对应的回调函数被 调用,然后在该回调函数中调用bufferevent_read()读数据。需要注意的是读写缓冲区中使用的数据结构是队列,意味着数据读完 在读缓冲区中该数据就不存在了 (2)使用bufferevent_write()向事件缓冲区中写数据,然后该数据就会自动被发送出去 //使用带缓冲区的事件进行套接字通信步骤---主要是listen_cb回调函数的实现过程 (1)创建带缓冲区的事件---该缓冲区对应读缓冲区和写缓冲区,读和写分别对应一个回调 struct bufferevent *bufferevent_socket_new( struct event_base *base, evutil_socket_t fd, enum bufferevent_options op ); 其中op使用BEV_OPT_CLOSE_ON_FREE即可,表示释放bufferevent时候关闭底层传输接口,这将关闭底层套接字、释放底层bufferevent等 (2)给读写缓冲区设置回到函数 void bufferevent_setcb( struct bufferevent *bufev, //bufferevent_socket_new()函数的返回值 bufferevent_data_cb readcb, //读缓冲区对应的回调函数,当缓冲区中有数据的时候自动调用该回调,需要自己定义一个回调中使用bufferevent_read()实现读操作 bufferevent_data_cb writecb(), //基本不用,写NULL即可 bufferevent_event_cb eventcd, //判断一些事件,如断开连接事件、连接成功事件,不同的事件对应不同的回调函数 void *cbarg ); 其中bufferevent_data_cb回调函数的原型为: typedef void(*bufferevent_data_cb)( struct bufferevent *bev, void *ctx ); bufferevent_event_cb对应的回调函数原型为: typedef void(*bufferevent_event_cb)( struct bufferevent *bev, short events, //通过这个参数来判断不同的事件 void *ctx ); events参数: BEV_EVENT_READING 读取操作发生时发生某事件,具体哪种事件要看其他标志 BEV_EVENT_WRITING 写入操作发生时发生某事件,具体哪种事件要看其他标志 BEV_EVENT_ERROR 操作发生错误,关于错误信息可以使用EVUTIL_SOCKET_ERROR()查看 BEV_EVENT_TIMEOUT 发生超时 BEV_EVENT_EOF 遇到文件结束提示 BEV_EVENT_CONNECTED 请求连接过程已经完成---在客户端中使用,可以判断连接是否成功 (3)在bufferevent上启动连接---客户端使用,即客户端连接服务器 int bufferevent_socket_connect( struct bufferevent *bev, //用于通信的文件描述符存储在bev中 struct sockaddr *address, //server端的ip和端口 int addrlen //地址长度---sizeof(sockaddr) ); (4)禁用缓冲区---禁用之后,对应的回调就不会被调用了 void bufferevent_disable( struct bufferevent *bufev, short events //要禁用那个事件对应的缓冲区 ); (5)启用缓冲区 void bufferevent_enable( struct bufferevent *bufev, short events ); 其中(4)和(5)中的events可以设置为EV_READ、EV_WRITE、或者EV_READ|EV_WRITE 注意:默认写缓冲区是可用的、读缓冲区是关闭的 (6)服务端需要进行的操作 通常socket通信服务端需要创建用于监听的文件描述符、绑定、监听、等待并接受连接,但是在libevent中使用连接监听器 evconnlistener即可完成以上四步 struct evconnlistener* evconnlistener_new_bind( //内部会创建一个文件描述符与ip和端口绑定 struct event_base *base, //事件处理框架 evconnlistener_cb cb, //接受连接后执行要处理的动作 void *ptr, //给回调函数evconnlistener_cb传参 unsigned flags, //可以设置为LEV_OPT_CLOSE_ON_FREE做一些底层释放;LEV_OPT_REUSEABLE端口复用 int backlog, //监听的最大客户端数目,默认128(也可以传-1表示使用默认值) const struct sockaddr *sa, //服务器的ip和端口信息 int socklen //sizeof(sockaddr) ); evconnlistener_cb回调函数原型: typedef void(*evconnlistener_cb)( struct evconnlistener *listener, //evconnlistener_new_bind()的返回值 evutil_socket_t sock, //用于监听或者是用于通信的文件描述符 struct sockaddr *addr, //客户端的IP和端口信息 int len, void *ptr //通过evconnlistener_new_bind()中的形参ptr传入 ); (7)server端关闭的时候要释放连接监听器的资源 void evconnlistener_free(struct evconnlistener *lev); */
服务端实现代码:
1 //使用event读fifo管道(有名管道) 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <arpa/inet.h> 10 #include <event2/event.h> 11 #include <event2/bufferevent.h> 12 #include <event2/listener.h> 13 14 //读回调 15 void read_cb(struct bufferevent *bev,void *ctx){ 16 //读bev缓冲区中的数据 17 char buf[1024]={0}; 18 bufferevent_read(bev,buf,sizeof(buf)); 19 printf("recv buf:%s\n",buf); 20 //发送数据---即想bev写缓冲区中写数据,然后会自动调用写回调发送数据 21 char* pt = "你发送的数据我已收到..."; 22 bufferevent_write(bev,pt,strlen(pt)+1); 23 printf("我发送了数据给客户端...\n"); 24 } 25 26 //写回调 27 void write_cb(struct bufferevent *bev,void *ctx){ 28 printf("发送的数据已经被发送出去了..."); 29 } 30 31 //事件回调 32 void event_cb(struct bufferevent *bev,short events,void *ctx){ 33 if(events & BEV_EVENT_EOF) 34 printf("connection closed\n"); 35 else if(events & BEV_EVENT_ERROR) 36 printf("some other error\n"); 37 //释放资源 38 bufferevent_free(bev); //调用这个回调了肯定是发生错误了 39 } 40 41 //连接完成后对应的通信操作 42 void listen_cb (struct evconnlistener *listener,evutil_socket_t fd,struct sockaddr *addr,int len,void *ptr){ 43 //得到传入的事件框架 44 struct event_base* base=(struct event_base*)ptr; 45 46 /*接受数据*/ 47 //01将fd封装为带缓冲区的事件 48 struct bufferevent *bev=NULL; 49 bev=bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE); 50 //02给bev的读写缓冲区分别设置回调函数 51 bufferevent_setcb(bev,read_cb,write_cb,event_cb,NULL); 52 //03由于默认bev的读缓冲区是disable的,所以要开启bev中的读缓冲区;写默认是开启的 53 bufferevent_enable(bev,EV_READ); 54 } 55 56 int main(int argc,char* argv[]){ 57 //创建事件处理框架 58 struct event_base *base=event_base_new(); 59 60 //server基本信息 61 struct sockaddr_in serv; 62 memset(&serv,0,sizeof(serv)); 63 serv.sin_family=AF_INET; 64 serv.sin_port=htons(8765); 65 inet_pton(AF_INET,"0.0.0.0",&serv.sin_addr.s_addr); 66 67 68 //创建监听的套接字、绑定、监听、接受新连接 69 //有新连接的时候,listen_cb回调函数被调用,其中第三个参数base是要传给listen_cb的最后一个参数ptr 70 struct evconnlistener *listen=NULL; 71 listen = evconnlistener_new_bind(base,listen_cb,base,LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 72 -1,(struct sockaddr*)&serv,sizeof(serv)); 73 74 //进入事件循环 75 event_base_dispatch(base); 76 77 //释放资源 78 evconnlistener_free(listen); 79 event_base_free(base); 80 81 return 0; 82 }
随便找一个以前的客户端代码去测试一下,注意要将客户端的要连接到的端口该为8765
刚刚测试了一下可以实现并发啊
4、使用libevent创建客户端
步骤
1 /* 2 使用Libevent创建客户端的步骤 3 (1)创建事件处理框架---使用event_base_new()函数; 4 struct event_base *base=event_base_new(); 5 (2)创建事件 6 01)首先要使用socket()创建文件描述符fd 7 int fd=socket(AF_INET,SOCK_STREAM,0); 8 02)创建带缓冲区的事件并将fd绑定到事件中 9 struct bufferevent *bev=bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE); 10 (3)连接服务器 11 01)首先要初始化一个结构体,这个结构体保存了要连接到的服务器的IP和端口 12 struct sockaddr_in serv; 13 memset(&serv,0,sizeof(serv)); 14 serv.sin_family=AF_INET; 15 serv.sin_port=htons(8765); 16 inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr); 17 02)连接至服务器,因为bev中绑定了fd,所以这里类似于将fd连接到server端 18 bufferevent_socket_connect(bev,(struct sockaddr*)&serv,sizeof(serv)); 19 03)给bev的读写缓冲区分别设置回调函数 20 bufferevent_setcb(bev,read_cb,write_cb,event_cb,NULL); 21 04)默认bev的读缓冲区是disable的,所以要开启bev中的读缓冲区;写默认是开启的 22 bufferevent_enable(bev,EV_READ); 23 (4)启动事件循环 24 event_base_dispatch(base); 25 (5)释放资源 26 event_base_free(base); 27 28 如果要接受键盘输入的话,要在启动事件循环之前加上如下步骤 29 01)接受键盘输入(将STDIN_FILENO文件描述符也封装到事件处理框架base中)---使用不带缓冲区的事件即可,即使用event_new() 30 其中read_terminal是对应的回调函数 31 STDIN_FILENO对应read_terminal()第一个形参fd;EV_READ|EV_PERSIST对应read_terminal()第二个形参what 32 bev对应read_terminal()中的最后一个形参arg 33 struct event* ev=event_new(base,STDIN_FILENO,EV_READ|EV_PERSIST,read_terminal,bev); 34 02)把事件ev添加到事件处理框架base中 35 event_add(ev,NULL); 36 37 */
客户端实现代码
1 //使用libevent使用客户端 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <arpa/inet.h> //for inet_pton() 10 #include <event2/event.h> 11 #include <event2/bufferevent.h> 12 13 //读回调 14 void read_cb(struct bufferevent *bev,void *ctx){ 15 //接受对方发送过来的数据 16 char buf[1024]={0}; 17 int len=bufferevent_read(bev,buf,sizeof(buf)); //len为读到字节数 18 printf("recv buf:%s\n",buf); 19 //给对方发数据 20 //bufferevent_write(bev,buf,len); //只要写到bev缓冲区中了,那么会立马发送到客户端,不写write_cb()也是可以的 21 } 22 23 //写回调 24 void write_cb(struct bufferevent *bev,void *ctx){ 25 printf("I am no useable\n"); 26 } 27 28 //事件回调 29 void event_cb(struct bufferevent *bev,short events,void *ctx){ 30 if(events & BEV_EVENT_EOF) 31 printf("connection closed\n"); 32 else if(events & BEV_EVENT_ERROR) 33 printf("some other error\n"); 34 else if(events & BEV_EVENT_CONNECTED){ 35 printf("已经成功连接至服务器...\n"); 36 return; 37 } 38 //否则就是出现了问题需要释放资源 39 bufferevent_free(bev); //调用这个回调了肯定是发生错误了 40 } 41 42 //接受终端输入,并将数据发送给服务端---将STDIN_FILENO文件描述符也封装到事件处理框架base中 43 //what为文件描述符对应的事件,arg为参数,fd对应于event_new()第二个参数即STDIN_FILENO(第七十行) 44 void read_terminal(evutil_socket_t fd,short what,void* arg){ 45 //读终端数据 46 char buf[1024]={0}; 47 int len = read(fd,buf,sizeof(buf)); 48 //将数据发给server 49 struct bufferevent* bev=(struct bufferevent*)arg; 50 bufferevent_write(bev,buf,len+1); 51 } 52 53 int main(int argc,char* argv[]){ 54 //(1)创建事件处理框架 55 struct event_base *base=event_base_new(); 56 //(2)创建事件--连接服务器 57 int fd=socket(AF_INET,SOCK_STREAM,0); 58 struct bufferevent *bev=NULL; 59 bev=bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE); //将fd搞进事件中,BEV_OPT_CLOSE_ON_FREE意思是在适当时刻将fd关闭 60 //(2.1)连接服务器---首先要初始化一个结构体,这个结构体保存了要连接到的服务器的IP和端口 61 struct sockaddr_in serv; 62 memset(&serv,0,sizeof(serv)); 63 serv.sin_family=AF_INET; 64 serv.sin_port=htons(8765); 65 inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr); 66 bufferevent_socket_connect(bev,(struct sockaddr*)&serv,sizeof(serv)); 67 //(2.2)给bev的读写缓冲区分别设置回调函数,只要读缓冲区有数据,read_cb回调立马被调用 68 bufferevent_setcb(bev,read_cb,write_cb,event_cb,NULL); 69 //(2.3)默认bev的读缓冲区是disable的,所以要开启bev中的读缓冲区;写默认是开启的 70 bufferevent_enable(bev,EV_READ); 71 72 /*(3)接受键盘输入---使用不带缓冲区的事件即可,相对终端来说执行读操作,并且是持续读 73 参数解释:STDIN_FILENO对应read_terminal()第一个形参fd;EV_READ|EV_PERSIST对应read_terminal()第二个形参what 74 bev对应read_terminal()中的最后一个形参arg 75 */ 76 struct event* ev=event_new(base,STDIN_FILENO,EV_READ|EV_PERSIST,read_terminal,bev); 77 //(3.1)把事件ev添加到事件处理框架base中 78 event_add(ev,NULL); 79 80 //(4)启动事件循环 81 event_base_dispatch(base); 82 83 //(5)释放资源 84 event_base_free(base); 85 86 return 0; 87 }
1 //使用event读fifo管道(有名管道) 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <arpa/inet.h> 10 #include <event2/event.h> 11 #include <event2/bufferevent.h> 12 #include <event2/listener.h> 13 14 //读回调 15 void read_cb(struct bufferevent *bev,void *ctx){ 16 //读bev缓冲区中的数据 17 char buf[1024]={0}; 18 bufferevent_read(bev,buf,sizeof(buf)); 19 printf("recv buf:%s\n",buf); 20 //发送数据---即想bev写缓冲区中写数据,然后会自动调用写回调发送数据 21 char* pt = "你发送的数据我已收到..."; 22 bufferevent_write(bev,pt,strlen(pt)+1); //只要写到bev缓冲区中了,那么会立马发送到客户端,不写write_cb()也是可以的 23 printf("我发送了数据给客户端...\n"); 24 } 25 26 //写回调 27 void write_cb(struct bufferevent *bev,void *ctx){ 28 printf("发送的数据已经被发送出去了..."); 29 } 30 31 //事件回调 32 void event_cb(struct bufferevent *bev,short events,void *ctx){ 33 if(events & BEV_EVENT_EOF) 34 printf("connection closed\n"); 35 else if(events & BEV_EVENT_ERROR) 36 printf("some other error\n"); 37 //释放资源 38 bufferevent_free(bev); //调用这个回调了肯定是发生错误了 39 } 40 41 //连接完成后对应的通信操作,fd是用于通信的文件描述符,这里的ptr参数对应evconnlistener_new_bind()中的第三个参数 42 void listen_cb (struct evconnlistener *listener,evutil_socket_t fd,struct sockaddr *addr,int len,void *ptr){ 43 //得到传入的事件框架 44 struct event_base* base=(struct event_base*)ptr; 45 46 /*接受数据*/ 47 //01将fd封装为带缓冲区的事件 48 struct bufferevent *bev=NULL; 49 bev=bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE); 50 //02给bev的读写缓冲区分别设置回调函数,只要读缓冲区有数据,read_cb回调立马被调用 51 bufferevent_setcb(bev,read_cb,write_cb,event_cb,NULL); 52 //03由于默认bev的读缓冲区是disable的,所以要开启bev中的读缓冲区;写默认是开启的 53 bufferevent_enable(bev,EV_READ); 54 } 55 56 int main(int argc,char* argv[]){ 57 //创建事件处理框架 58 struct event_base *base=event_base_new(); 59 60 //server基本信息 61 struct sockaddr_in serv; 62 memset(&serv,0,sizeof(serv)); 63 serv.sin_family=AF_INET; 64 serv.sin_port=htons(8765); 65 inet_pton(AF_INET,"0.0.0.0",&serv.sin_addr.s_addr); 66 67 68 //创建监听的套接字、绑定、监听、接受新连接 69 //有新连接的时候,listen_cb回调函数被调用,其中第三个参数base是要传给listen_cb的最后一个参数ptr 70 struct evconnlistener *listen=NULL; 71 listen = evconnlistener_new_bind(base,listen_cb,base,LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 72 -1,(struct sockaddr*)&serv,sizeof(serv)); 73 74 //启动事件循环 75 event_base_dispatch(base); 76 77 //释放资源 78 evconnlistener_free(listen); 79 event_base_free(base); 80 81 return 0; 82 }
上面的服务端代码和上面的是一样的,只不过添加了一些注释
执行结果:
六、使用epoll实现Web服务器
1、http协议介绍
HTTP协议处于应用层,主要包括两部分:请求消息和响应消息
1.1请求消息
请求消息---浏览器向服务器发送请求消息,该请求消息包括四部分:请求行、请求头、空行、请求数据
- 请求行:说明请求类型、要访问的数据、以及使用的http版本
- 请求头:说明服务器要使用的附加信息,如该浏览器是要使用的语言、压缩格式、连接状态:保持连接
- 空行:是必须有的,即使没有请求数据
- 请求数据:也叫主体,可以添加任意的其他数据
下面单独的介绍一下请求行:
1)请求行---请求类型
GET方法用户提交的一些信息会显示在地址栏,比如用户提交给要访问服务器的用户名密码都会显示在地址栏,所以缺点是不安全;
POST方法在地址栏不会显示用户提交的一些信息,故在表单提交的时候使用POST方法更安全一些
2)请求行---请求数据
如:在地址栏输入192.168.1.115/表示要访问ip为192.168.1.115的服务器的根目录中的资源
假如服务器中开放的资源目录为/home/kevin/Documents,那么浏览器访问的就是该目录下的资源
如:在地址栏输入192.168.1.115/hello.c那么就是访问服务器中目录为/home/kevin/Documents/hello.c中的资源
3)请求行---http版本--一般是要HTTP/1.1版本
4)请求行示例:
1 GET请求方法将请求数据放在请求类型中,即GET后面的3.txt 2 GET /3.txt HTTP/1.1 <!--请求行--> 其中GET为请求类型、3.txt为要访问服务器中的资源、HTTP/1.1表示http版本(三者之间有空格) 3 Host:localhost:2.2.2.2 <!--请求头(主要是一些键值对)(GET方法将请求数据放在请求类型中)--> 4 User-Agent:Mozilla/5.0/21001 Firefox/24.0 5 Accept:text/html.application/xhtml+xml.application 6 Accept-Language:zh-cn,zh; 7 空行()\r\n) <!--空行--> 8 <!--请求数据为空-->
1 POST方法将请求数据放在请求数据中,如: 2 POST HTTP/1.1 <!--请求行--> 3 Host:localhost:2.2.2.2 <!--请求头--> 4 User-Agent:Mozilla/5.0/21001 Firefox/24.0 5 Accept:text/html.application/xhtml+xml.application 6 Accept-Language:zh-cn,zh; 7 空行(\r\n) <!--空行--> 8 3.txt <!--请求数据不为空-->
举例:
浏览器中的地址栏输入: 192.168.199.128:8989/hello.c 浏览器会将此封装成一个http请求协议(默认GET方法): GET/hello.c HTTP/1.1 key:value key:value key:value key:value \r\n
1.2响应消息
响应消息---服务器给浏览器发,包括:状态行、消息报头、空行、响应正文
- 状态行:包括http协议版本号、状态码、状态信息
- 消息报头:说明客户端要使用的一些附加信息
- 空行:空行是必须有的
- 响应正文:服务器返回给客户端的文本信息
举例:
1 HTTP/1.1 200 OK\r\n <!--状态行(200:状态码、OK不是固定的YES也是可以的)--> 2 Server:micro_httped\r\n <!--以下是一些键值对--> 3 Date:Fri,18,jul 2014 12:34:26 GMT\r\n 4 Content-Type:text/plain;charset=iso-8859-1\r\n <!--http中的文件类型(必须的,告诉浏览器要发送的数据类型)text/plain对应文本文件;charset=iso-8859-1表示获取到的资源的编码方式,如果要显式中文设置方法为charset=utf-8--> 5 Content-Length:32\r\n <!--要发送的数据长度,如果不知道长度可以写-1,也可以不写--> 6 \r\n 7 //这里写上要发送的数据
1.3状态码
状态码由三位数字组成,第一个数字定义了响应的类别,共分为五种:
(1)1xx:指示信息---表示已接收,继续处理
(2)2xx:成功---表示已被成功接收、理解
(3)3xx:重定向---要完成请求必须进行更进一步的操作
(4)4xx:客户端错误---请求有语法错误或请求无法实现 404:要访问的资源不存在、403:服务器收到请求但是决绝服务
(5)5xx:服务器端错误---服务器未能完成实现合法的请求
常见状态码:
200:客户端请求成功
400:客户端请求有无法错误,不能被服务器所理解
401:请求未经授权,不能被服务器所理解
404:要访问的资源不存在
403:服务器收到请求但是决绝服务
500:服务器发生了不可预期的错误
503:服务器当前不能处理客户端的请求,一段时间后可能恢复正常
2、Web服务器实现步骤(B/S架构)
2.1Web服务器实现步骤及实现代码
Web服务器实现步骤
1 void main(){ 2 //01)修改服务器准备公开的资源目录---path可以通过命令行的方式传入 3 chdir(path); 4 5 //02)创建用于监听的文件描述符lfd 6 int lfd=socket(); 7 //03)将用于监听的文件描述符和IP、端口进行绑定 8 bind(); 9 //04)监听 10 listen(lfd,64); 11 //05)创建一个epoll树的根节点 12 int epfd=epoll_create(2000); 13 //06)将lfd对应的epoll_event结构体对象ev添加到epfd为根节点的树上 14 epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); 15 //07)将下来的代码放在一个while(1)循环中,循环检测发生变化了的文件描述符,参数-1表示epoll_wait()一直阻塞 16 int num=epoll_wait(epfd,all,MAXSIZE,-1) 17 //08)如果有文件描述符状态发生了变化,那么epoll_wait()停止阻塞,并将发生变化了的文件描述符个数返回给num,发生变化了的文件描述符复制到all中 18 //接下来使用num遍历all数组 19 08.1)如果all[i]是监听的文件描述符 20 08.1.1)执行int cfd=accept(),并将cfd对应的epoll_event结构体对象添加到epoll数上 21 08.2)如果all[i]是通信的文件描述符 22 08.2.1)读数据---该数据是浏览器发送过来的请求信息,然后再给浏览器发送响应信息过去 23 24 }
实现代码:
1 //epoll_Webserver.h 2 #ifndef _EPOLL_SERVER_H 3 #define _EPOLL_SERVER_H 4 5 void epoll_run(int port); 6 int init_listen(int port,int epfd); 7 void do_accept(int lfd,int epfd); 8 void do_read(int cfd,int epfd); 9 int get_line(int fd,char* buf,int size); 10 void http_request(const char* request,int cfd); 11 void send_respose_head(int cfd,int num,const char* desp,const char* type,long len); 12 void send_file(int cfd,const char* filename); 13 void send_dir_methodOne(int cfd,const char* dirname); 14 void send_dir_methodTwo(int cfd,const char* dirname); 15 void disconnect(int cfd,int epfd); 16 void encode_str(char* to,int tosize,const char* from); //编码---如将"国"准换为%e5%9b%bd 17 void decode_str(char* to,char* from); //解码---如将%e5%9b%bd转换为"国" 18 int hexit(char s); 19 20 #endif
1 //epoll_Webserver.c 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include <sys/types.h> //for setsockopt() 7 #include <sys/stat.h> //for stat()判断一个路径是文件还是目录 8 #include <sys/epoll.h> 9 #include <fcntl.h> //for fcntl() 10 #include <dirent.h> //for opendir() 11 #include <arpa/inet.h> //for inet_pton() 12 #include <ctype.h> //for isxdigit() 13 #include "epoll_Webserver.h" 14 15 #define MAXSIZE 2000 16 17 void epoll_run(int port){ 18 //创建一个epoll树的根节点 19 int epfd=epoll_create(MAXSIZE); 20 if(epfd==-1){ 21 perror("epoll_creat error\n"); 22 exit(1); 23 } 24 //添加要监听的节点 25 int lfd=init_listen(port,epfd); 26 27 //委托内核检测添加到树上的节点 28 struct epoll_event all[MAXSIZE]; 29 while(1){ 30 int ret=epoll_wait(epfd,all,MAXSIZE,-1); //如果内核检测的文件描述符由发生变化了的,那么复制到all数组中;-1表示如果没有文件描述符发生变化,那么epoll_wait()一直阻塞 31 if(ret==-1){ //并将发生变化了的文件描述符的个数返回给ret 32 perror("epoll_wait error"); 33 exit(1); 34 } 35 //遍历发生变化了的文件描述符 36 for(int i=0;i<ret;++i){ 37 //只处理读事件,其他事件默认不处理 38 struct epoll_event *pev=&all[i]; 39 if(!(pev->events & EPOLLIN)){ //如果不是读事件 40 continue; 41 } 42 //如果all[i]是监听的文件描述符 43 if(pev->data.fd==lfd){ 44 //接受连接请求 45 do_accept(lfd,epfd); 46 } 47 //如果all[i]是通信的文件描述符 48 else{ 49 //读数据---用于通信的文件描述符存储在了pev指向的epoll_event结构体中的data联合体中的fd中 50 do_read(pev->data.fd,epfd); 51 } 52 } 53 } 54 55 } 56 57 //初始化用于监听的套接字 58 int init_listen(int port,int epfd){ 59 int lfd=socket(AF_INET,SOCK_STREAM,0); 60 if(lfd==-1){ 61 perror("socket error"); 62 exit(1); 63 } 64 //lfd绑定本地IP和端口 65 struct sockaddr_in serv; 66 serv.sin_family=AF_INET; 67 serv.sin_port=htons(port); 68 //inet_pton(AF_INET,"192.168.199.128",&serv.sin_addr.s_addr); 69 serv.sin_addr.s_addr = htonl(INADDR_ANY); 70 71 //端口复用 72 int flag=1; 73 setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag)); 74 75 int ret=bind(lfd,(struct sockaddr*)&serv,sizeof(serv)); 76 if(ret==-1){ 77 perror("bind error"); 78 exit(1); 79 } 80 //监听 81 ret=listen(lfd,64); 82 if(ret==-1){ 83 perror("listne error"); 84 exit(1); 85 } 86 //将lfd对应的epoll_event结构体对象添加到epfd为根节点的树上 87 struct epoll_event ev; 88 ev.events=EPOLLIN; 89 ev.data.fd=lfd; 90 ret=epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); 91 if(ret==-1){ 92 perror("epoll_ctl error"); 93 exit(1); 94 } 95 96 return lfd; 97 } 98 99 void do_accept(int lfd,int epfd){ 100 struct sockaddr_in client; //用于服务端中保存已连接了的客户端的信息,client是一个传出参数 101 int len=sizeof(client); 102 int cfd=accept(lfd,(struct sockaddr*)&client,&len); 103 if(cfd==-1){ 104 perror("accept error"); 105 exit(1); 106 } 107 //打印客户端信息 108 char ip[64]={0}; 109 printf("New Client IP:%s,Port:%d,cfd=%d\n", 110 inet_ntop(AF_INET,&client.sin_addr.s_addr,ip,sizeof(ip)), 111 ntohs(client.sin_port),cfd); 112 //设置cfd为非阻塞 113 int flag = fcntl(cfd,F_GETFL); //获取cfd原属性 114 flag|=O_NONBLOCK; 115 fcntl(cfd,F_SETFL,flag); //设置文件描述符cfd为非阻塞方式---即在读缓冲区没有数据的时候read()不会发生阻塞 116 //将得到的cfd挂到epoll树上 117 struct epoll_event ev; 118 ev.events=EPOLLIN | EPOLLET; //读事件,并设置为边沿模式 119 ev.data.fd=cfd; 120 int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev); 121 if(ret==-1){ 122 perror("epoll_ctl error"); 123 exit(1); 124 } 125 } 126 127 void do_read(int cfd,int epfd){ 128 //将浏览器发送过来的数据读到buf中 129 char line[1024]={0}; 130 //第一次读的是请求行 131 int len=get_line(cfd,line,sizeof(line)); 132 if(len==0){ 133 printf("客户端断开连接\n"); 134 //关闭套接字,从epoll树上删除将cfd对应的epoll_event结构体对象删除 135 disconnect(cfd,epfd); 136 } 137 else if(len == -1){ 138 perror("recv error"); 139 exit(1); 140 } 141 else{ 142 printf("========请求行数据========\n"); 143 printf("%s\n",line); 144 //没读完则继续读 145 printf("========请求头========\n"); 146 while(len>0){ 147 char buf[1024]={0}; 148 len=get_line(cfd,buf,sizeof(buf)); //读请求头(键值对)到buf中 149 printf("%s",buf); 150 } 151 } 152 /*请求行的格式:GET /xxx http1.1 153 判断line中的前三个字符是不是GET请求:使用strncasecmp(const char* s1,const char* s2,size_t n)函数 154 比较s2的前n个字符是不是和s1相等,函数中case的意思是不区分s1和s2的大小写,如果匹配则返回0,否则返回-1 155 */ 156 157 if(strncasecmp("get",line,3) == 0){ 158 //处理http的请求 159 http_request(line,cfd); 160 } 161 } 162 163 /*解析http请求消息中的每一行内容---http中的请求消息中的每一行都是以\r\n结尾的,如: 164 Get /3.txt HTTP/1.1\r\n <!--请求行---注意GET、3.txt和HTTP1.1三者之间有空格--> 165 Host:localhost:2.2.2.2\r\n <!--请求头:一些键值对--> 166 User-Agent:Mozilla/5.0/21001 Firefox/24.0\r\n 167 Accept:text/html.application/xhtml+xml.application\r\n 168 Accept-Language:zh-cn,zh;\r\n 169 \r\n <!--空行--> 170 \r\n <!--请求数据:GET请求方法中请求数据为空--> 171 172 */ 173 int get_line(int fd,char* buf,int size){ 174 int i=0; 175 char c='\0'; 176 int n; 177 while((i<size-1) && (c!='\n')){ 178 n=recv(fd,&c,1,0); //先读一个字符到c中(以剪切的方式读) 179 if(n>0){ 180 if(c=='\r'){ 181 n=recv(fd,&c,1,MSG_PEEK); //先试探一下读缓冲区中还有多少个数据(以拷贝的方式读) 182 if((n>0) && (c=='\n')) 183 recv(fd,&c,1,0); //读到的有可能不是\n,那么在else中直接将c赋值为\n 184 else 185 c='\n'; //结束while循环 186 } 187 buf[i]=c; //将读到的一个字符保存到buf中 188 ++i; 189 } 190 else 191 c='\n'; 192 } 193 buf[i]='\0'; 194 if(n==-1){ //recv()返回值为-1,让get_line()返回值也是-1 195 i=-1; 196 } 197 return i; 198 } 199 200 //处理浏览器发送过来的http的请求信息 201 void http_request(const char* request,int cfd){ //此时request中没有\n了,因为get_line()函数中已经将\r去掉了 202 //拆分http的请求行 203 char method[12],path[1024],protocol[12]; //分别表示请求方法、请求访问服务器的资源路径、和HTTP协议版本号 204 sscanf(request,"%[^ ] %[^ ] %[^ ]",method,path,protocol); //%[^ ]表示匹配非空格以外的全部字符,注意%[^ ] %[^ ] %[^ ]三者之间也有空格,对应Get /3.txt HTTP/1.1\r\n 三者之间的空格 205 printf("method=%s,path=%s,protocol=%s\n",method,path,protocol); 206 207 /*如果path中含有中文,浏览器会对中文进行解码操作,那么服务器需要对其进行解码(即将%e4%b8%ad%e5%9b%bd转换为汉字) 208 第一个path是目的操作数(包含汉字的目录),第二个path是元操作数(包含汉字的编码的目录)。 209 第二个path指向不变 210 */ 211 decode_str(path,path); 212 213 //处理path=/xxx中的/是没有用的,这是因为在主函数一开始就已经进入了服务器的资源目录中,所以要去掉这个/ 214 char* file=path+1; //去掉path中的/ 215 //如果没有指定访问的资源,默认显式资源目录中的文件 216 if(strcmp(path,"/") == 0){ 217 //file的值就是资源目录的当前位置 218 file="./"; 219 } 220 221 //判断file是目录还是文件,其中st是一个传出参数,包含了路径file的信息 222 struct stat st; 223 int ret=stat(file,&st); 224 if(ret==-1){ 225 //show 404也是可以的 226 perror("stat error"); 227 exit(1); 228 } 229 //如果file是目录---使用函数S_ISDIR()判断,是则返回1否则返回0 230 if(S_ISDIR(st.st_mode)){ 231 //发送信息头;text/html表示要发送给浏览器的是一个html文件,-1表示长度任意 232 send_respose_head(cfd,200,"OK","text/html",-1); 233 //发送目录信息 234 send_dir_methodTwo(cfd,file); 235 } 236 //如果是文件---使用函数S_ISREG()判断,是则返回1否则返回0 237 else if(S_ISREG(st.st_mode)){ 238 //打开打开文件之后需要发送响应消息给浏览器,所以在打开文件之前要先发送状态行和消息报头;text/plain表示要发送的文件格式是txt 239 send_respose_head(cfd,200,"OK","text/plain",st.st_size); 240 //打开文件,并将文件中的内容发送给浏览器 241 send_file(cfd,file); 242 } 243 } 244 245 /*给浏览器发送响应头 246 1)cfd为通信用的文件描述符 247 2)num为状态行中的状态码(如200) 248 3)desp为状态码对应的字符串 249 4)type为键值对中的键Content-Type对应的值---即要发送给浏览器的文件的格式 250 5)len为键值对中的键Content-Length对应的值---即要发送的数据的长度 251 */ 252 void send_respose_head(int cfd,int num,const char* desp,const char* type,long len){ 253 char buf[1024]={0}; 254 //状态行:如HTTP/1.1 200 OK的形式,http大小写都是可以的 255 sprintf(buf,"http/1.1 %d %s\r\n",num,desp); 256 send(cfd,buf,strlen(buf),0); 257 //消息报头---一些键值对,其中Content-Type和Content-Length是必不可少的 258 sprintf(buf,"Content-Type:%s\r\n",type); 259 send(cfd,buf,strlen(buf),0); 260 sprintf(buf,"Content-Length:%ld\r\n",len); 261 send(cfd,buf,strlen(buf),0); 262 //发送空行 263 send(cfd,"\r\n",2,0); 264 } 265 266 //给浏览器发送文件 267 void send_file(int cfd,const char* filename){ 268 //打开文件 269 int fd=open(filename,O_RDONLY); 270 if(fd==-1){ 271 //show 404 272 return; 273 } 274 //循环读文件 275 char buf[1024]={0}; 276 int len; 277 while(len = read(fd,buf,sizeof(buf)) > 0){ 278 //发送读出的数据 279 send(cfd,buf,len,0); 280 } 281 if(len == -1){ 282 perror("read file error"); 283 exit(1); 284 } 285 close(cfd); 286 } 287 288 //发送目录方法一 289 void send_dir_methodOne(int cfd,const char* dirname){ 290 //拼一个html页面:使用<table></table>显式多行多列 291 char buf[4094]={0}; //buf大小为4K 292 sprintf(buf,"<html><head><title>目录名:%s</title></head>",dirname); 293 sprintf(buf+strlen(buf),"<body>h1当前目录:%s</h1><table>",dirname); 294 #if 1 //#if后面是数字1 295 DIR* dir=opendir(dirname); 296 if(dir==NULL){ 297 perror("opendir error"); 298 exit(1); 299 } 300 //由于文件存储方式是树状存储,所以相当于去读头结点为filename的二叉树中的每一个节点 301 struct dirent* ptr=NULL; 302 while((ptr=readdir(dir)) != NULL){ 303 sprintf(buf,"<tr>"); 304 char* name=ptr->d_name; 305 306 } 307 closedir(dir); 308 #endif 309 } 310 311 //发送目录方法二 312 void send_dir_methodTwo(int cfd,const char* dirname){ 313 //拼一个html页面:使用<table></table>显式多行多列 314 char buf[4094]={0}; //buf大小为4K 315 sprintf(buf,"<html><head><title>Webserver%s</title></head>",dirname); 316 sprintf(buf+strlen(buf),"<body><h1>CurrentIndex:%s</h1><table>",dirname); 317 318 //目录二级指针 319 struct dirent** ptr; 320 int num=scandir(dirname,&ptr,NULL,alphasort); //alphasort表示返回的文件夹按照名字进行排序 321 char path[1024]={0}; 322 char enstr[1024]={0}; //编码保存buf 323 for(int i=0;i<num;++i){ 324 char* name=ptr[i]->d_name; 325 //为了达到name文件夹的大小等信息,需要得到name文件夹的完整路径,接下来进行目录的拼接 326 sprintf(path,"%s/%s",dirname,name); //由于在main函数中已经转到了dirname的上一级目录,所以这里进行目录的拼接只拼接到dirname即可 327 struct stat st; 328 stat(path,&st); 329 //如果是文件---使用函数S_ISREG 330 //如果是文件---首先对其进行编码---即将包含汉字的目录转换为包含3个十六进制的目录 331 encode_str(enstr,sizeof(enstr),name); 332 if(S_ISREG(st.st_mode)){ 333 sprintf(buf+strlen(buf),"<tr><td><a href=\"%s/\">%s/</a></td><td>%ld</td></tr>", //<a href="a"></a>表示创建一个a的超链接需要使用\对"进行转义 334 enstr,name,st.st_size); //enstr是给浏览器的,name是浏览器页面上给用户看的 335 } 336 //如果是目录 337 else if(S_ISDIR(st.st_mode)){ 338 sprintf(buf+strlen(buf),"<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>", //<a href="a"></a>表示创建一个a的超链接需要使用\对"进行转义 339 enstr,name,st.st_size); 340 } 341 //每循环一次把buf中的内容发送出去,以防止缓冲区满了 342 send(cfd,buf,sizeof(buf),0); 343 memset(buf,0,sizeof(buf)); 344 } 345 sprintf(buf+strlen(buf),"</table></body></html>"); 346 send(cfd,buf,sizeof(buf),0); 347 printf("dir message send OK!!\n"); 348 } 349 350 //关闭套接字,从epoll树上将cfd对应的epoll_event结构体对象删除 351 void disconnect(int cfd,int epfd){ 352 int ret=epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL); 353 if(ret==-1){ 354 perror("epoll_ctl delete cfd error"); 355 exit(1); 356 } 357 close(cfd); 358 } 359 360 //编码 361 void encode_str(char* to,int tosize,const char* from){ 362 int tolen; 363 for(tolen=0;*from!='\0' && tolen+4<tosize;++from){ 364 if(isalnum(*from) || strchr("/_.-~",*from)!=(char*)0){ 365 *to=*from; 366 ++to; 367 ++tolen; 368 } 369 else{ 370 sprintf(to,"%%%02x",((int)(*from)) & 0xff); 371 to+=3; 372 tolen+=3; 373 } 374 } 375 *to='\0'; 376 } 377 378 //解码---用作服务器给浏览器发送数据的时候,将字符、数字和/_.-~以外的字符转义(主要是将中文转成三个字节的十六进制形式) 379 void decode_str(char* to,char* from){ 380 for(;*from!='\0';++to,++from){ 381 if(from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])){ 382 //将十六进制转换为十进制(字符对应的就是十进制,如'a'=65) 383 *to=hexit(from[1])*16+hexit(from[2]); 384 from+=2; 385 } 386 else{ 387 *to=*from; 388 } 389 } 390 *to='\0'; 391 } 392 //16进制转换为10进制 393 int hexit(char s){ 394 if(s>='0' && s<='9'){ 395 return s-'0'; 396 } 397 else if(s>='a' && s<='z'){ 398 return s-'a'+10; 399 } 400 else if(s>='A' && s<='Z'){ 401 return s-'A'+10; 402 } 403 return 0; 404 }
1 //服务器开发 2 3 //ldd xxx //xxx为可执行文件,ldd的作用是查看不存在的链接库 4 5 /* 6 HTML---超文本标记语言 7 在计算机中以.html或.htm作为扩展名 8 可以被浏览器访问,就是经常见到的网页 9 10 HTTP协议处于应用层,主要包括两部分:请求消息和响应消息 11 1、请求消息---浏览器向服务器发送请求消息,该请求消息包括四部分:请求行、请求头、空行、请求数据 12 请求行:说明请求类型、要访问的数据、以及使用的http版本 13 请求头:说明服务器要使用的附加信息,如该浏览器是要使用的语言、压缩格式、连接状态:保持连接 14 空行:是必须有的,即使没有请求数据 15 请求数据:也叫主体,可以添加任意的其他数据 16 17 1)请求行---请求类型 18 GET方法用户提交的一些信息会显示在地址栏,比如用户提交给要访问服务器的用户名密码都会显示在地址栏,所以缺点是不安全; 19 POST方法在地址栏不会显示用户提交的一些信息 20 故在表单提交的时候使用POST方法更安全一些 21 2)请求行---请求数据 22 如:在地址栏输入192.168.1.115/表示要访问ip为192.168.1.115的服务器的根目录中的资源 23 加入服务器中开放的资源目录为/home/kevin/Documents,那么浏览器访问的就是该目录下的资源 24 如:在地址栏输入192.168.1.115/hello.c那么就是访问服务器中目录为/home/kevin/Documents/hello.c中的资源 25 3)请求行---http版本--一般是要HTTP/1.1版本 26 27 4)请求行示例: 28 GET请求方法将请求数据放在请求类型中,即GET后面的3.txt 29 GET /3.txt HTTP/1.1 其中GET为请求类型、3.txt为要访问服务器中的资源、HTTP/1.1表示http版本(三者之间有空格) 30 Host:localhost:2.2.2.2 <!--请求行(GET方法将请求数据放在请求行中)--> 31 User-Agent:Mozilla/5.0/21001 Firefox/24.0 32 Accept:text/html.application/xhtml+xml.application 33 Accept-Language:zh-cn,zh; 34 空行 <!--空行--> 35 <!--请求数据为空--> 36 37 POST方法将请求数据放在请求数据中,如: 38 POST HTTP/1.1 <!--请求行--> 39 Host:localhost:2.2.2.2 <!--请求头--> 40 User-Agent:Mozilla/5.0/21001 Firefox/24.0 41 Accept:text/html.application/xhtml+xml.application 42 Accept-Language:zh-cn,zh; 43 空行 <!--空行--> 44 3.txt <!--请求数据不为空--> 45 46 浏览器中的地址栏输入: 47 192.168.199.128:8989/hello.c 48 浏览器会将此封装成一个http请求协议(默认GET方法): 49 GET/hello.c HTTP/1.1 50 key:value 51 key:value 52 key:value 53 key:value 54 \r\n 55 56 2、响应消息---服务器给浏览器发,包括:状态行、消息报头、空行、响应正文 57 状态行:包括http协议版本号、状态码、状态信息 58 消息报头:说明客户端要使用的一些附加信息 59 空行:空行是必须有的 60 响应正文:服务器返回给客户端的文本信息 61 例: 62 HTTP/1.1 200 OK\r\n <!--状态行(200:状态码、OK不是固定的YES也是可以的)--> 63 Server:micro_httped\r\n <!--以下是一些键值对--> 64 Date:Fri,18,jul 2014 12:34:26 GMT\r\n 65 Content-Type:text/plain;charset=iso-8859-1\r\n <!--http中的文件类型(必须的,告诉浏览器要发送的数据类型)text/plain对应文本文件;charset=iso-8859-1表示获取到的资源的编码方式,如果要显式中文设置方法为charset=utf-8--> 66 Content-Length:32\r\n <!--要发送的数据长度,如果不知道长度可以写-1,也可以不写--> 67 \r\n 68 //这里写上要发送的数据 69 70 71 状态码由三位数字组成,第一个数字定义了响应的类别,共分为五种: 72 (1)1xx:指示信息---表示已接收,继续处理 73 (2)2xx:成功---表示已被成功接收、理解 74 (3)3xx:重定向---要完成请求必须进行更进一步的操作 75 (4)4xx:客户端错误---请求有语法错误或请求无法实现 404:要访问的资源不存在、403:服务器收到请求但是决绝服务 76 (5)5xx:服务器端错误---服务器未能完成实现合法的请求 77 78 常见状态码: 79 200:客户端请求成功 80 400:客户端请求有无法错误,不能被服务器所理解 81 401:请求未经授权,不能被服务器所理解 82 404:要访问的资源不存在 83 403:服务器收到请求但是决绝服务 84 500:服务器发生了不可预期的错误 85 503:服务器当前不能处理客户端的请求,一段时间后可能恢复正常 86 87 3、Web服务器实现 88 由于浏览器在应用层使用的是http协议,而http底层使用的是tcp协议,所以 89 void http_respond_head(int cfd,char* type){ 90 char buf[1024]; 91 //状态行 92 sprintf(buf,"heep/1.1 200 OK\r\n") 93 write(cfd,buf,sizeof(buf)); 94 //消息报头--一些键值对 95 sprintf(buf,"Content-type:%s\r\n",type); //要发送的文件类型---这个键值对是必须的 96 write(cfd,buf,sizeof(buf)); 97 //空行 98 write(cfd,"\r\n",2); 99 100 } 101 void main(){ 102 //01)修改服务器准备公开的资源目录---path可以通过命令行的方式传入 103 chdir(path); 104 105 //02)创建用于监听的文件描述符lfd 106 int lfd=socket(); 107 //03)将用于监听的文件描述符和IP、端口进行绑定 108 bind(); 109 //04)监听 110 listen(lfd,64); 111 //05)创建一个epoll树的根节点 112 int epfd=epoll_create(2000); 113 //06)将lfd对应的epoll_event结构体对象ev添加到epfd为根节点的树上 114 epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev); 115 //07)将下来的代码放在一个while(1)循环中,循环检测发生变化了的文件描述符,参数-1表示epoll_wait()一直阻塞 116 int num=epoll_wait(epfd,all,MAXSIZE,-1) 117 //08)如果有文件描述符状态发生了变化,那么epoll_wait()停止阻塞,并将发生变化了的文件描述符个数返回给num,发生变化了的文件描述符复制到all中 118 //接下来使用num遍历all数组 119 08.1)如果all[i]是监听的文件描述符 120 08.1.1)执行int cfd=accept(),并将cfd对应的epoll_event结构体对象添加到epoll数上 121 08.2)如果all[i]是通信的文件描述符 122 08.2.1)读数据---该数据是浏览器发送过来的请求信息,然后再给浏览器发送响应信息过去 123 124 } 125 4、服务器和浏览器都需要使用unicode对汉子进行编码和解码 126 unicode安装方法:sudo apt-get install unicode 127 使用方法:unicode 中 就会返回3个字节的十六进制数:e4 b8 ad 128 5、recv()中的参数设置 129 int ret=recv(int fd,char* buf,int n,int flag) 130 flag=MSG_PEEK的时候表示以拷贝的方式从缓冲区读数据 131 flag=0表示以剪切的方式从缓冲区读数据 132 举例:假如读缓冲区中有数据:1234567890 133 recv(fd,buf,sizeof(buf),0)----读完以后缓冲区没有数据了 134 recv(fd,buf,sizeof(buf),MSG_PEEK)---读完以后缓冲区还要数据, 135 flag=MSG_PEEK一般用来试探一下缓冲区中有多少数据(根绝返回值的大小),得到缓冲区中有多少数据后,再使用flag=0去读缓冲区中的数据 136 6、sscanf(const char *buffer,const char *format,[argument]...)---读取格式化字符串中的数据 137 buffer表示要拆分的字符串,format表示拆分的规则,举例: 138 (1)%[^ ]表示匹配非空格以外的全部字符 139 sscanf("123456 abcdefg","%[^ ]",buf); 140 printf("%s\n",buf); //输出123456 141 (2)%[1-9]表示匹配数字1-9;%[a-z]表示匹配字母a-z;则%[1-9a-z]表示匹配数字1-9和字母a-z;其中%[1-9a-z]和%[^A-Z]匹配是一样的 142 sscanf("123456abcdBCD abcdefg","%[^ ]",buf); 143 printf("%s\n",buf); //输出123456abcd 144 (3)sscanf("GET /Linux_code2 http/1.1","%[^ ] %[^ ] %[^ ]",method,path,pro); 145 method中保存的是GET、path中保存的是/Linux_code2、pro中保存的是http/1.1 146 scanf() 将终端输入保存到buf中 147 7、检查一个路径是文件还是目录用的函数 148 #include <sys/stat.h> 149 int stat(const char* pathname,struct stat *st); //将获取到的文件属性放到stat结构体对象st中 150 其中stat结构体中有一个成员为st_mode(short类型)存储了路径的信息,再通过以下函数判断是目录还是文件: 151 S_ISDIR(st.st_mode)函数判断file是目录与否如果返回值为1说明是,否则不是; 152 S_ISREG(st.st_mode)函数判断file是文件与否,如果返回值为1说明是,否则不是。 153 8、打开目录函数一: 154 #include <dirent.h> 155 DIR* opendir(const char* filename); //DIR是一个结构体变量类型 156 打开父目录为filename的文件夹,返回值为一个DIR指针 157 然后再使用下面的函数打开=父目录为filename的子文件夹: 158 struct dirent* readdir(DIR *dirp); 159 opendir()和readdir()函数使用方法举例: 160 DIR* dir=opendir(dirname); //打开父文件夹dirname,dirname的类型为const char* dirname 161 struct dirent* ptr=NULL; 162 ptr=readdir(dir) 163 char* name=ptr->d_name; //获取打开的子文件夹的名字 164 9、打开目录函数二 165 #include <dirent.h> 166 int scandir(const char *dirp,struct dirent ***namelist, //三级指针指向二级指针的地址 167 int (*filter)(const struct dirent *), 168 int (*compar)(const struct dirent **,const struct dirent **)); 169 第一个参数dirp:要打开的目录,注意sandir()只扫描当前目录,不扫描dirp的子目录 170 第二个参数namelist:传出参数 171 使用的时候可以定义一个二级指针struct dirent** ptr或者是struct dirent* ptr[]; 传入的时候对ptr取地址即可 172 对于struct dirent* ptr[]数组中每个元素都是目录项指针(struct dirent*),每个目录项指针都指向一块内存,这些内存中存储了目录为dirp的自文件夹信息 173 第三个参数:filter是一个函数指针 174 要过滤掉的目录项 175 第四个参数:compar文件夹显式的时候,指定排序方法。Linux提供了这些方法: 176 int alphasort(const struct dirent **a,const struct dirent **b); //按照首字母进行排序 177 int versionsort(const struct dirent **a,const struct dirent **b); //按照版本号进行排序 178 返回值为子文件夹的个数 179 scandir()使用方法举例(目标是遍历父目录为dirname的全部子文件或子文件夹): 180 struct dirent** ptr; //定义一个dirent二级指针ptr,由于scandir()第二个形参是一个dirent三级指针,那么对ptr取地址传入即可 181 int num=scandir(dirname,&ptr,NULL,alphasort); //alphasort表示返回的文件夹按照名字进行排序 182 char path[1024]={0}; 183 for(int i=0;i<num;++i){ //遍历ptr[i] 184 char* name=ptr[i]->d_name; 185 //接下来判断ptr[i]是文件夹还是文件 186 sprintf(path,"%s/%s",dirname,name); //为了达到name文件夹的大小等信息,需要得到name文件夹的完整路径,需要进行目录的拼接 187 struct stat st; //由于在main函数中已经转到了dirname的上一级目录,所以这里进行目录的拼接只拼接到dirname即可 188 stat(path,&st); 189 //如果是文件 190 if(S_ISREG(st.st_mode)){...} 191 //如果是目录 192 else if(S_ISDIR(st.st_mode)){...} 193 } 194 195 196 10、#if 0/#if 1 ... #endif的作用 197 #if 0 //数字0,被处理器处理 198 code1 199 #endif 200 code2 201 上面的代码中,code1永远不会被执行,而执行的是code2 202 #if 1 //数字1,被处理器处理 203 code1 204 #endif 205 code2 206 上面的代码中,code2永远不会被执行,而执行的是code1 207 11、http协议会将中文分解为3个字节,比如一个"国"字会分解为e5 9b bd三个字节 208 那么浏览器在收到含有中文资源名字的时候,会将这些中文转换为三个字节的数据并在每一个字节前加一个%,发给服务器 209 如在浏览器地址栏输入:192.168.199.128/中国 那么浏览器会将中国两个字转换为:%e4%b8%ad%e5%9b%bd 210 那么服务器在收到浏览器发送过来的请求信息后,需要对%e4%b8%ad%e5%9b%bd进行解码 211 server需要编码吗? 212 需要,相当于服务器把浏览器编码的工作给干了 213 12、编码和解码相关函数: 214 isalnum(char s); 判断字符s是否是数字0~9,如果是则返回1,否则返回0 215 strchr(const char* s1, char s2); 查看字符串s1中是否含有字符s2,如果有 216 int isxdigit(int c); 检查参数c是否是16进制数字,如果是十六进制数字,那么返回非0值,否则返回0;需要包含的头文件为#include <ctype.h> 217 参数c为int类型,但是也可以传入char类型的值,如: 218 isxdigit('a'); 返回非0数字 219 isxdigit('A'); 返回非0数字 220 221 */ 222 //epoll实现Web服务器 223 #include <stdio.h> 224 #include <unistd.h> 225 #include <stdlib.h> 226 #include <sys/types.h> 227 #include <sys/stat.h> 228 #include <string.h> 229 #include <fcntl.h> 230 #include <arpa/inet.h> //for inet_pton() 231 #include "epoll_Webserver.h" 232 233 int main(int argc,char* argv[]){ 234 if(argc < 3){ 235 printf("输入参数不够\n"); 236 exit(1); 237 } 238 //输入的端口为字符串,需要转换 239 int port=atoi(argv[1]); 240 241 //修改进程的工作目录 242 int ret=chdir(argv[2]); //成功则返回0,否则返回-1 243 if(ret==-1){ 244 perror("chdir error"); //可能是由于输入的目录不存在 245 exit(1); 246 } 247 epoll_run(port); 248 249 return 0; 250 }
2.2和普通client、server(C/S架构)的区别
B/S架构中包含了C/S架构中要使用的创建套接字、绑定、监听、接收连接、读和发送数据的步骤,只不过B/S架构中要在读操作中读取的是按照http协议来写的数据,那么就需要对浏览器发送过来的数据提取有用信息,那么得使用一系列的函数,比如下面要介绍的函数。B/S架构要发送的数据也不是随意的,也要按照http响应消息的规范来书写响应消息,然后使用send()函数发送出去。
总的来说,B/S架构需要使用对浏览器发送过来的数据进行字符串分解,分解出浏览器要访问服务器中的资源,然后根据服务器当前目录,打开浏览器要访问的资源,如果浏览器要访问的资源不在服务器当前路径下,那么返回404错误。再就服务器获取到浏览器要访问的资源后,要依次使用目录打开函数打开对应的资源、再然后发送响应消息。
3、相关函数介绍
3.1 recv()中的参数设置(使用recv()的第三个参数决定是采用剪切方式还是以拷贝方式从缓冲区读数据)
recv()函数原型:
int ret=recv(int fd,char* buf,int n,int flag)
flag=MSG_PEEK的时候表示以拷贝的方式从缓冲区读数据
flag=0表示以剪切的方式从缓冲区读数据
举例:假如读缓冲区中有数据:1234567890
1 recv(fd,buf,sizeof(buf),0)----读完以后缓冲区没有数据了 2 recv(fd,buf,sizeof(buf),MSG_PEEK)---读完以后缓冲区还要数据, 3 flag=MSG_PEEK一般用来试探一下缓冲区中有多少数据(根绝返回值的大小),得到缓冲区中有多少数据后,再使用flag=0去读缓冲区中的数据
3.2 使用正则表达式获取请求消息中浏览器要访问服务器资源的字符串---使用sscanf()
sscanf函数原型:
sscanf(const char *buffer,const char *format,[argument]...)
buffer表示要拆分的字符串,format表示拆分的规则,第三个参数可以跟若干个保存分解来的字符串buf,举例:
1 (1)%[^ ]表示匹配非空格以外的全部字符 2 sscanf("123456 abcdefg","%[^ ]",buf); 3 printf("%s\n",buf); //输出123456 4 (2)%[1-9]表示匹配数字1-9;%[a-z]表示匹配字母a-z;则%[1-9a-z]表示匹配数字1-9和字母a-z;其中%[1-9a-z]和%[^A-Z]匹配是一样的 5 sscanf("123456abcdBCD abcdefg","%[^ ]",buf); 6 printf("%s\n",buf); //输出123456abcd 7 (3)sscanf("GET /Linux_code2 http/1.1","%[^ ] %[^ ] %[^ ]",method,path,pro); 8 method中保存的是GET、path中保存的是/Linux_code2、pro中保存的是http/1.1
3.3 检查一个路径是文件还是目录用的函数---stat()
stat()函数原型
1 #include <sys/stat.h> 2 int stat(const char* pathname,struct stat *st); //将获取到的文件属性放到stat结构体对象st中
其中stat结构体中有一个成员为st_mode(short类型)存储了路径的信息,再通过以下函数判断是目录还是文件:
S_ISDIR(st.st_mode)函数判断file是目录与否如果返回值为1说明是,否则不是;
S_ISREG(st.st_mode)函数判断file是文件与否,如果返回值为1说明是,否则不是。
3.4 打开目录函数一:opendir()
opendir()函数原型
#include <dirent.h> DIR* opendir(const char* filename); //DIR是一个结构体变量类型
打开父目录为filename的文件夹,返回值为一个DIR指针,然后再使用下面的函数打开=父目录为filename的子文件夹:
1 struct dirent* readdir(DIR *dirp); 2 opendir()和readdir()函数使用方法举例: 3 DIR* dir=opendir(dirname); //打开父文件夹dirname,dirname的类型为const char* dirname 4 struct dirent* ptr=NULL; 5 ptr=readdir(dir) 6 char* name=ptr->d_name; //获取打开的子文件夹的名字
3.5 打开目录函数二:scandir() 注该函数使用起来较简单
scandir()函数原型
#include <dirent.h> int scandir(const char *dirp,struct dirent ***namelist, //三级指针指向二级指针的地址 int (*filter)(const struct dirent *), int (*compar)(const struct dirent **,const struct dirent **));
参数说明:
/* 第一个参数dirp:要打开的目录,注意sandir()只扫描当前目录,不扫描dirp的子目录 第二个参数namelist:传出参数 使用的时候可以定义一个二级指针struct dirent** ptr或者是struct dirent* ptr[]; 传入的时候对ptr取地址即可 对于struct dirent* ptr[]数组中每个元素都是目录项指针(struct dirent*),每个目录项指针都指向一块内存,这些内存中存储了目录为dirp的自文件夹信息 第三个参数:filter是一个函数指针 要过滤掉的目录项 第四个参数:compar文件夹显式的时候,指定排序方法。Linux提供了这些方法: int alphasort(const struct dirent **a,const struct dirent **b); //按照首字母进行排序 int versionsort(const struct dirent **a,const struct dirent **b); //按照版本号进行排序 返回值为子文件夹的个数 */
scandir()使用方法举例(目标是遍历父目录为dirname的全部子文件或子文件夹):
1 struct dirent** ptr; //定义一个dirent二级指针ptr,由于scandir()第二个形参是一个dirent三级指针,那么对ptr取地址传入即可 2 int num=scandir(dirname,&ptr,NULL,alphasort); //alphasort表示返回的文件夹按照名字进行排序 3 char path[1024]={0}; 4 for(int i=0;i<num;++i){ //遍历ptr[i] 5 char* name=ptr[i]->d_name; 6 //接下来判断ptr[i]是文件夹还是文件 7 sprintf(path,"%s/%s",dirname,name); //为了达到name文件夹的大小等信息,需要得到name文件夹的完整路径,需要进行目录的拼接 8 struct stat st; //由于在main函数中已经转到了dirname的上一级目录,所以这里进行目录的拼接只拼接到dirname即可 9 stat(path,&st); 10 //如果是文件 11 if(S_ISREG(st.st_mode)){...} 12 //如果是目录 13 else if(S_ISDIR(st.st_mode)){...} 14 }
3.6 浏览器、服务器中队函数的编码和解码的函数(自己定义)
http协议会将中文分解为3个字节,比如一个"国"字会分解为e5 9b bd三个字节
那么浏览器在收到含有中文资源名字的时候,会将这些中文转换为三个字节的数据并在每一个字节前加一个%,发给服务器
如在浏览器地址栏输入:192.168.199.128/中国 那么浏览器会将中国两个字转换为:%e4%b8%ad%e5%9b%bd
那么服务器在收到浏览器发送过来的请求信息后,需要对%e4%b8%ad%e5%9b%bd进行解码
server需要编码吗?
需要,相当于服务器把浏览器编码的工作给干了
具体的编码解码函数,见epoll_Webserver.c中的encode()、decode()函数
3.7 #if 0/#if 1 ... #endif的作用
1 #if 0 //数字0,被处理器处理 2 code1 3 #endif 4 code2 5 上面的代码中,code1永远不会被执行,而执行的是code2 6 #if 1 //数字1,被处理器处理 7 code1 8 #endif 9 code2