目录
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 !!! "); 56 char ipbuf[64] = {0}; 57 printf("client IP: %s, port: %d ", 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(" 客户端已经断开了连接 "); 74 close(cfd); 75 break; 76 } 77 else 78 { 79 printf("recv buf: %s ", buf); 80 // 转换 - 小写 - 大写 81 for(int i=0; i<len; ++i) 82 { 83 buf[i] = toupper(buf[i]); 84 } 85 printf("send buf: %s ", 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 "); 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("请输入要发送的字符串: "); 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("服务端关闭了连接 "); 45 break; 46 } 47 else{ 48 printf("reveive buf: %s ",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 ",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 ",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("客户端断开了连接 "); 107 close(cfd); 108 break; 109 } 110 else 111 { 112 printf("recv buf:%s ",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 ",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断开了连接 ",client_port); 44 close(info->fd); 45 break; 46 } 47 else{ 48 printf("client:%d message:%s ",client_port,buf); 49 write(info->fd,buf,len); //再给客户端写回去 50 buf[0] = '