1. 基本知识 从应用的角度出发,协议可理解为“规则”,是数据传输和数据的解释的规则。
1.1 协议 1.典型的协议
2.分层模型结构
3.网络传输流程
注意:数据没有封装之前,是不能在网络中传递的。
4.ARP协议
在网络通讯时,源主机的应用程序知道目的主机的IP地址和端口号,却不知道目的主机的硬件地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP协议就起到这个作用,源主机发出ARP协议,询问IP地址是192.168.0.1的主机的硬件地址是多少,并将这个请求广播到本地网段,目的主机接收到广播ARP请求,发现其中的IP地址与本机相符,则发送一个ARP应答数据包给源主机,将自己的硬件地址填写在应答包中。
ARP协议:根据IP地址获取mac地址
以太网帧协议:根据mac地址,完成数据包传输
5.IP协议:
补:IP地址:可以在网络环境中,唯一标识一台主机
端口号:可以网络的一台主机上,唯一标识一个进程
IP地址+端口号:可以在网络环境中,唯一标识一个进程
1.2 C/S模型与B/S模型 1.C/S模型(client-server)
优点:缓存大量数据、协议选择灵活、速度快
缺点:安全性低、不能跨平台、开发工作量较大
2.B/S模型
优点:安全性高、跨平台、开发工作量较小
缺点:不能缓存大量数据、严格遵守HTTP
2. 网络 2.1 网络套接字socket
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)。在通信过程中,套接字一定是成对出现的。
1.字节序
2.转换函数
3.IP地址转换函数
本地字节序(string IP) ——-> 网络字节序
int inet_pton(int af , const char *src , void *dst);
网络字节序—>本地字节序(string IP)
const char *inet_ntop(int af , const void *src , char *dst , socklen_t size);
参1:AF_INET、AF_INET6
参2:网络字节序IP地址
参3:本地字节序(string IP)
参4:参3的大小
返回值:成功(参3);失败(NULL);
4.sockaddr地址结构(查看:man 7 ip)
1 2 3 4 5 struct sockaddr_in addr ;addr.sin_family = AF_INET/AF_INET6 addr.sin_port = htons(9527 ); addr.sin_addr.s_addr = htonl(INADDR_ANY); bind(fd,(struct sockaddr *)&addr,size);
2.2 网络相关函数 1.创建一个套接字
头文件:#include <sys/socket.h>
int socket(int domain , int type , int protocol);
参1:AF_INET(IPV4)、AF_INET6(IPV6)、AF_UNIX(本地套接字)
参2:SOCK_STREAM(流式协议)、SOCK_DGRAM(报式协议)
参3:0即可
返回值:成功(新套接字所对应文件描述符);失败(-1)
2.给socket绑定一个地址结构(IP+port)
头文件:include <arpa/inet.h>
int bind(int sockfd , const struct sockaddr *addr , socklen_t addrlen);
参1:socket函数返回值
参2:传入参数,自己的地址结构(IP+port)
参3:地址结构的大小
返回值:成功(0),失败(-1)
3.设置同时与服务器建立连接的上限数(同时进行三次握手的客户端数量)
int listen(int socket , int backlog);
参1:socket函数返回值
参2:上限数值。最大值为128
返回值:成功(0);失败(-1)
4.阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符
int accept(int sockfd , struct sockaddr *addr , socklen_t *addrlen);
参1:socket函数返回值
参2:传出参数,得到成功与服务器建立连接的那个客户端的地址结构
参3:传入传出参数,传入参2的大小,传出客户端地址结构实际大小
返回值:成功(能与服务器进行数据通信的socket对应的文件描述符);失败(-1)
5.使用现有的socket与服务器建立连接
int connect(int sockfd , const struct sockaddr *addr , socklen_t addrlen)
参1:socket函数返回值
参2:传入参数,服务器的地址结构
参3:服务器的地址结构的大小
返回值:成功(0);失败(-1)
补:如果不使用bind绑定客户端地址结构,系统采用“隐式绑定”。
服务端实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #define SERV_PORT 9527 void sys_err (const char *str) { perror(str); exit (1 ); } int main () { int lfd = 0 ,cfd = 0 ; struct sockaddr_in serv_addr ,clit_addr ; char buf[BUFSIZ],client_IP[1024 ]; int ret ,i; socklen_t clit_addr_len; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); lfd = socket(AF_INET,SOCK_STREAM,0 ); if (lfd == -1 ){ sys_err("socket error" ); } bind(lfd,(struct sockaddr *)&serv_addr,sizeof (serv_addr)); listen(lfd,128 ); clit_addr_len = sizeof (clit_addr); cfd = accept(lfd,(struct sockaddr *)&clit_addr,&clit_addr_len); if (cfd == -1 ){ sys_err("accept error" ); } printf ("client ip:%s port:%d\n" , inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr,client_IP,sizeof (client_IP)), ntohs(clit_addr.sin_port)); while (1 ){ ret = read(cfd,buf,sizeof (buf)); write(STDOUT_FILENO,buf,ret); for (i = 0 ; i < ret; i++){ buf[i] = toupper (buf[i]); } write(cfd,buf,ret); } close(lfd); close(cfd); return 0 ; }
客户端实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #define SERV_PORT 9527 void sys_err (const char *str) { perror(str); exit (1 ); } int main () { int cfd; int conter = 10 ; char buf[BUFSIZ]; struct sockaddr_in serv_addr ; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET,"192.168.88.93" ,&serv_addr.sin_addr.s_addr); cfd = socket(AF_INET,SOCK_STREAM,0 ); if (cfd == -1 ){ sys_err("socket error" ); } int ret = connect(cfd,(struct sockaddr *)&serv_addr,sizeof (serv_addr)); while (--conter){ write(cfd,"hello\n" ,6 ); ret = read(cfd,buf,sizeof (buf)); write(STDOUT_FILENO,buf,ret); sleep(1 ); } close(cfd); return 0 ; }
2.3 三次握手和四次挥手 1.三次握手
主动发起连接请求端:发送SYN标志位,请求建立连接。携带序号号、数据字节数(0)、滑动窗口大小win。
被动接受连接请求端:发送ACK(应答)标志位,同时携带SYN请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小win。
主动发起连接请求端:发送ACK标志位,应答服务器连接请求,携带确认序号。
2.四次挥手
3.滑动窗口:发送给连接的对端,本端的缓冲区大小(实时),保证数据不会丢失。
4.read函数的返回值:
2.3 多进程多线程的并发服务器 案例:多进程并发服务器实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 void catch_child (int signum) { while (waitpid(0 ,NULL ,WNOHANG)>0 ); return ; } int main () { int lfd = Socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in srv_addr ; bzero(&srv_addr,sizeof (srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_port = htons(9527 ); srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); Bind(lfd,(struct sockaddr *)&srv_addr,sizeof (srv_addr)); Listen(lfd,128 ); struct sockaddr_in clit_addr ; socklen_t clit_len; clit_len = sizeof (clit_addr); pid_t pid; char buf[1024 ]; int i; int cfd; while (1 ){ cfd = Accept(lfd,(struct sockaddr *)&clit_addr,&clit_len); pid = fork(); if (pid<0 ){ perror("fork error" ); exit (-1 ); }else if (pid==0 ){ close(lfd); break ; }else { close(cfd); struct sigaction act ; act.sa_handler = catch_child; sigemptyset(&act.sa_mask); act.sa_flags = 0 ; int ret = sigaction(SIGCHLD,&act,NULL ); if (ret!=0 ){ perror("sigaction error" ); exit (-1 ); } continue ; } } if (pid==0 ){ for (;;){ int ret = read(cfd,buf,sizeof (buf)); if (ret == 0 ){ close(cfd); exit (1 ); } for (i=0 ;i<ret;i++){ buf[i]=toupper (buf[i]); } write(cfd,buf,ret); write(STDOUT_FILENO,buf,ret); } } return 0 ; }
2.4多线程并发服务器实现 案例:多线程并发服务器实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 struct s_info { struct sockaddr_in cliaddr ; int connfd; }; void *do_work (void *arg) { int n,i; struct s_info *ts = (struct s_info*)arg; char buf[1024 ]; char clie_ip[16 ]; while (1 ){ n = read(ts->connfd,buf,1024 ); if (n == 0 ){ printf ("the client %d closed ....\n" ,ts->connfd); break ; } printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET,&(*ts).cliaddr.sin_addr.s_addr,clie_ip,sizeof (clie_ip)), ntohs((*ts).cliaddr.sin_port)); for (i=0 ;i<n;i++){ buf[i] = toupper (buf[i]); } write(STDOUT_FILENO,buf,n); write(ts->connfd,buf,n); } close(ts->connfd); return (void *)0 ; } int main () { int listenfd = Socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in servaddr ; bzero(&servaddr,sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9527 ); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); Bind(listenfd,(struct sockaddr *)&servaddr,sizeof (servaddr)); Listen(listenfd,128 ); printf ("accept client connect ....\n" ); struct sockaddr_in cliaddr ; socklen_t cliaddr_len; struct s_info ts [256]; int i=0 ; pthread_t tid; while (1 ){ cliaddr_len = sizeof (cliaddr); int connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len); ts[i].cliaddr = cliaddr; ts[i].connfd = connfd; pthread_create(&tid,NULL ,do_work,(void *)&ts[i]); pthread_detach(tid); i++; } return 0 ; }
2.5 TCP状态转换 1.查看网络状态
2.TCP状态转换图
主动发送请求连接端:CLOSED —– 发送SYN —– SYN_SEND —– 接收ACK、SYN —– SYN_SEND —– 发送ACK —– ESTABLISHED(数据通信态)
主动关闭请求连接端:ESTABLISHED(数据通信态) —– 发送FIN —– FIN_WAIT_1 —– 接收ACK —– FIN_WAIT_2(半关闭) —– 接收对端发送FIN —– FIN_WAIT_2(半关闭) —– 回发ACK —– TIME_WAIT(只有主动关闭连接方,会经历该状态) —– 等2MSL时长(40秒) —–CLOSED
被动接收连接请求端:CLOSK —– LISTEN —– 接收SYN —– LISTEN —– 发送ACK、SYN(对方收到) —– SYN_RCVD —– 接收ACK —– ESTABLISHED(数据通信态)
被动关闭连接请求端:ESTABLISHED(数据通信态)—–接收FIN —– ESTABLISHED —– 发送ACK —– CLOSE_WAIT(说明主动连接端处于半关闭状态) —– 发送FIN —– LAST_ACK —– 接收ACK —– CLOSED
注意:如果是客户端先关闭,服务端后关闭,则客户端会TIME_WAIT,服务端不会,所以又可以马上对服务端进行开启;如果是服务端先关闭,客户端后关闭,则服务端会进入TIME_WAIT状态,此时还没有进入完全关闭状态,会占用端口40秒,所以你这段时间内又开服务端会失败,因为端口号还被占用着。
补:2 MSL时长:一定出现在主动关闭连接请求端(TIME_WAIT);保证最后一个ACK能成功被对端接收(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求)。
3.端口复用
在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为TCP连接没有完全断开指的是connfd没有完全断开,而我们重新的是listenfd,虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server.c代码的socket()和bind()调用之间插入如下代码即可:
1 2 int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
4.半关闭
通信双方中,只有一端关闭通信。———> FIN_WAIT_2
close(fd);
shutdown(int fd,int how);
参2:SHUT_RD(关读端);SHUT_WR(关写端);SHUT_RDWR(关读写端);
注意:shutdown在关闭多个文件描述符应用的文件时(dup2重定向,多个文件描述符指向一个文件),采用全关闭方法(关闭一个相当于关闭了全部);close只关闭一个文件描述符,其他指向该文件的文件描述符还可以使用
3. 多路IO转接服务器
多路IO转接服务器也叫多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接取而代之由内核应用程序监视文件。主要使用的方法有三种:select、epoll、poll。
服务端accept客户端的三种方式:阻塞、非阻塞忙轮询、响应式(多路IO转接)
3.1 select多路IO转接 原理:借助内核,select来监听客户端连接、数据通信事件。
1.清空一个文件描述符集合
void FD_ZERO(fd_set *set);
使用:fd_set rset; FD_ZERO(&rset);
2.将待监听的文件描述符,添加到监听集合中
void FD_SET(int fd, fd_set *set);
使用:FD_SET(3,&rset); FD_SET(5,&rset); FD_SET(6,&rset);
3.将一个文件描述符从监听集合中移除
void FD_CLR(int fd, fd_set *set);
4.判断一个文件描述符是否在监听集合中
5.select监听
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参1:监听的所有文件描述符中,最大文件描述符+1;
参2:读文件描述符监听集合(传入传出参数);
参3:写文件描述符监听集合(传入传出参数); —>不用就NULL
参4:异常文件描述符监听集合(传入传出参数); —>不用就NULL
参5:大于0(设置监听超时时长);NULL(阻塞监听);0(非阻塞监听,轮询);
返回值:
6.select优缺点:
案例:使用select完成服务端的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 int main () { int listenfd = Socket(AF_INET,SOCK_STREAM,0 ); int opt = 1 ; setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt)); struct sockaddr_in serv_addr ; bzero(&serv_addr,sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(9527 ); Bind(listenfd,(struct sockaddr *)&serv_addr,sizeof (serv_addr)); Listen(listenfd,128 ); fd_set rset,allset; int ret; int i,j,n; char buf[1024 ]; struct sockaddr_in clie_addr ; socklen_t clie_addr_len; int connfd; int maxfd = listenfd; FD_ZERO(&allset); FD_SET(listenfd,&allset); while (1 ){ rset = allset; ret = select(maxfd+1 ,&rset,NULL ,NULL ,NULL ); if (ret<0 ){ perror("select error" ); exit (-1 ); } if (FD_ISSET(listenfd,&rset)){ clie_addr_len = sizeof (clie_addr); connfd = Accept(listenfd,(struct sockaddr *)&clie_addr,&clie_addr_len); printf ("创建连接成功connfd = %d\n" ,connfd); FD_SET(connfd,&allset); if (maxfd<connfd){ maxfd = connfd; } if (ret==1 ){ continue ; } } for (i=listenfd+1 ;i<=maxfd;i++){ if (FD_ISSET(i,&rset)){ n = read(i,buf,sizeof (buf)); if (n==0 ){ close(i); FD_CLR(i,&allset); }else if (n==-1 ){ perror("read error" ); } for (j=0 ;j<n;j++){ buf[j] = toupper (buf[j]); } write(i,buf,n); write(STDOUT_FILENO,buf,n); } } } close(listenfd); }
3.2 poll多路IO转接 1.poll监听函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2.poll的优缺点
3.突破1024文件描述符限制(对select不管用)
修改:打开vim /etc/security/limits.conf
写入:
soft nofile 65536 ———->设置默认值,可以直接借助命令修改
hard nofile 100000 ———–>命令修改上限
修改后需要注销用户,使其生效,相当于重新启动
案例:使用poll实现多路IO转接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 int main () { int listenfd = Socket(AF_INET,SOCK_STREAM,0 ); int opt = 1 ; setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt)); struct sockaddr_in serv_addr ; bzero(&serv_addr,sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(9527 ); Bind(listenfd,(struct sockaddr *)&serv_addr,sizeof (serv_addr)); Listen(listenfd,128 ); struct pollfd client [1024]; client[0 ].fd = listenfd; client[0 ].events = POLLIN; int i; for (i=1 ;i<1024 ;i++){ client[i].fd = -1 ; } int maxi = 0 ; int nready; struct sockaddr_in cliaddr ; socklen_t clilen; char client_ip[16 ]; int connfd; for (;;){ nready = poll(client,maxi+1 ,-1 ); if (client[0 ].revents & POLLIN){ clilen = sizeof (cliaddr); connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&clilen); printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,client_ip,sizeof (client_ip)), ntohs(cliaddr.sin_port)); int i; for (i=1 ;i<1024 ;i++){ if (client[i].fd<0 ){ client[i].fd = connfd; break ; } } if (i==1024 ){ perror("too many clients" ); exit (-1 ); } client[i].events = POLLIN; if (i>maxi){ maxi = i; } if (--nready<=0 ){ continue ; } } int sockfd; int i,j,n; char buf[1024 ]; for (i=1 ;i<=maxi;i++){ if ((sockfd = client[i].fd)<0 ){ continue ; } if (client[i].revents & POLLIN){ if ((n=read(sockfd,buf,1024 ))<0 ){ if (errno==ECONNRESET){ printf ("client[%d] aborted connection\n" ,i); }else { perror("read error" ); } close(sockfd); client[i].fd = -1 ; }else if (n == 0 ){ printf ("client[%d] closed connection\n" ,i); close(sockfd); client[i].fd = -1 ; }else { for (j=0 ;j<n;j++){ buf[j] = toupper (buf[j]); } write(sockfd,buf,n); write(STDOUT_FILENO,buf,n); } if (--nready<=0 ){ break ; } } } } return 0 ; }
3.3 epoll多路IO转接 1.int epoll_create(int size);
2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
3.int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
参1:epoll_create函数的返回值
参2:传出参数,是数组,满足监听条件的哪些fd结构体
参3:数组元素的总个数
参4:等于-1(阻塞);等于0(不阻塞);大于0:超时时间(毫秒)
返回值:大于0(满足监听的总个数,可以用作循环上限);等于0(没有fd满足监听事件);等于-1(失败,errno);
案例:使用epoll实现多路IO转接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 int main () { int listenfd = Socket(AF_INET,SOCK_STREAM,0 ); printf ("listenfd = %d\n" ,listenfd); int opt = 1 ; setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt)); struct sockaddr_in serv_addr ; bzero(&serv_addr,sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(9527 ); Bind(listenfd,(struct sockaddr *)&serv_addr,sizeof (serv_addr)); Listen(listenfd,128 ); int efd = epoll_create(39 ); printf ("efd = %d\n" ,efd); if (efd == -1 ){ perror("epoll_create" ); exit (0 ); } struct epoll_event tep ,ep [1024]; tep.events = EPOLLIN; tep.data.fd = listenfd; int res = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep); if (res == -1 ){ perror("epoll_ctl" ); exit (0 ); } struct sockaddr_in cliaddr ; socklen_t clilen; char buf[1024 ]; int num = 0 ; for (;;){ int nready = epoll_wait(efd,ep,1024 ,-1 ); if (nready == -1 ){ perror("epoll_wait error" ); exit (0 ); } int i; for (i=0 ;i<nready;i++){ if (ep[i].data.fd == listenfd){ clilen = sizeof (cliaddr); char client_ip[16 ]; int connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&clilen); printf ("received from %s at PORT %d\n" ,inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,client_ip,sizeof (client_ip)),ntohs(cliaddr.sin_port)); printf ("cfd %d----client %d\n" ,connfd,++num); tep.events = EPOLLIN; tep.data.fd = connfd; res = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep); if (res == -1 ){ perror("epoll_ctl_error" ); exit (0 ); } }else { int sockfd = ep[i].data.fd; int n = read(sockfd,buf,1024 ); if (n == 0 ){ res = epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL ); if (res == -1 ){ perror("epoll_ctl error" ); exit (-1 ); } close(sockfd); printf ("client[%d] closed connection\n" ,sockfd); }else if (n<0 ){ perror("read n<0 error:" ); res = epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL ); close(sockfd); }else { int j; for (j=0 ;j<n;j++){ buf[j] = toupper (buf[j]); } write(STDOUT_FILENO,buf,n); write(sockfd,buf,n); } } } } close(listenfd); close(efd); return 0 ; }
4.epoll事件模式
ET模式:边沿触发,缓冲区剩余未读尽的数据不会导致epoll_wait返回。新的事件满足,才会触发。
FT模式:水平触发(默认模式),缓冲区剩余未读尽的数据会导致epoll_wait返回。
注意:像select和epoll它们监听的都是文件描述符,不是套接字,所以可以结合管道来使用。
比较:FT是缺省的工作方式,并且同时支持阻塞和非阻塞;ET是高速工作方式,只支持非阻塞(所以ET常与非阻塞一起用)–>非阻塞通常搭配忙轮询。
结论:epoll的ET模式,高效模式,但是只支持非阻塞模式(no black)
1 2 3 4 5 6 struct epoll_event event ;event.events = EPOLLIN | EPOLLET; epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event); int flag = fcntl(cfd,F_GETFL);flag |= O_NONBLOCK; fcntl(cfd,F_SETFL,flg);
5.epoll的优缺点
epoll优点:高效,突破1024文件描述符
epoll缺点:不能跨平台,只能linux系统
案例:通过epoll和管道测试ET和FT模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 int main () { int efd,i; int pfd[2 ]; pid_t pid; char buf[MAXLINE],ch = 'a' ; pipe(pfd); pid = fork(); if (pid==0 ){ close(pfd[0 ]); while (1 ){ for (i=0 ;i<MAXLINE/2 ;i++){ buf[i] = ch; } buf[i-1 ] = '\n' ; ch++; for (;i<MAXLINE;i++){ buf[i] = ch; } buf[i-1 ] = '\n' ; ch++; write(pfd[1 ],buf,sizeof (buf)); sleep(5 ); } close(pfd[1 ]); }else if (pid>0 ){ struct epoll_event event; struct epoll_event resevent [10]; int res ,len; close(pfd[1 ]); efd = epoll_create(10 ); event.events = EPOLLIN; event.data.fd = pfd[0 ]; epoll_ctl(efd,EPOLL_CTL_ADD,pfd[0 ],&event); while (1 ){ res = epoll_wait(efd,resevent,10 ,-1 ); printf ("res %d\n" ,res); if (resevent[0 ].data.fd == pfd[0 ]){ len = read(pfd[0 ],buf,MAXLINE/2 ); write(STDOUT_FILENO,buf,len); } } close(pfd[0 ]); close(efd); }else { perror("fork" ); exit (-1 ); } return 0 ; }
4. 其它通信方式 4.1 TCP通信和UDP通信各自的优缺点 TCP:面向连接的,可靠数据报传输。对于不稳定的网络层,采取完全弥补的通信方式。丢包重传。
优点:数据流量稳定、传输速度稳定、顺序稳定
缺点:传输速度慢、效率低、资源开销大
使用场景:数据的完整型要求较高,不追求效率。(大数据传输、文件传输)
2.UDP:无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式。默认还原网络状况。
优点:传输速度快、效率高、资源开销小
缺点:数据流量不稳定、传输速度不稳定、顺序不稳定
使用场景:对时效性要求较高的场合,稳定性其次。(游戏、视频会议、视频电话)—–>腾讯、华为、阿里:应用层数据校验协议,弥补UDP的不足。
4.2 UDP通信 1.UDP中负责读数据函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
2.UDP中负责写数据函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
UDP服务端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #define BUFSIZ 1024 int main () { int sockfd = socket(AF_INET,SOCK_DGRAM,0 ); struct sockaddr_in serv_addr ; bzero(&serv_addr,sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(9527 ); bind(sockfd,(struct sockaddr *)&serv_addr,sizeof (serv_addr)); struct sockaddr_in clie_addr ; socklen_t clie_addr_len; int n; char buf[BUFSIZ]; char str_ip[16 ]; printf ("Accepting connections.....\n" ); while (1 ){ clie_addr_len = sizeof (clie_addr); n = recvfrom(sockfd,buf,BUFSIZ,0 ,(struct sockaddr *)&clie_addr,&clie_addr_len); if (n == -1 ){ perror("recvfrom error" ); } printf ("received from %s at port %d\n" , inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,str_ip,sizeof (str_ip)), ntohs(clie_addr.sin_port)); int i; for (i=0 ;i<n;i++){ buf[i] = toupper (buf[i]); } n = sendto(sockfd,buf,n,0 ,(struct sockaddr *)&clie_addr,sizeof (clie_addr)); if (n == -1 ){ perror("sendto error" ); } } close(sockfd); return 0 ; }
UDP客户端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #define BUFSIZ 1024 int main () { int sockfd = socket(AF_INET,SOCK_DGRAM,0 ); struct sockaddr_in servaddr ; bzero(&servaddr,sizeof (servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET,"192.168.88.93" ,&servaddr.sin_addr); servaddr.sin_port = htons(9527 ); int n; char buf[BUFSIZ]; while (fgets(buf,BUFSIZ,stdin ) != NULL ){ n = sendto(sockfd,buf,strlen (buf),0 ,(struct sockaddr *)&servaddr,sizeof (servaddr)); if (n == -1 ){ perror("sendto error" ); } n = recvfrom(sockfd,buf,BUFSIZ,0 ,NULL ,0 ); if (n == -1 ){ perror("recvfrom error" ); } write(STDOUT_FILENO,buf,n); } close(sockfd); return 0 ; }
4.3 本地套接字(domain) 本地套接字与网络套接字使用的区别:
网络套接字中用的AF_INET;本地套接字是用AF_UNIX/AF_LOCAL
网络套接字中的地址结构是sockaddr_in;本地套接字是用sockaddr_un
网络套接字中是绑定ip和端口,本地套接字是绑定socket文件即可(伪文件),类似于管道(但不是)
补:本地套接字只能用于本机的两个进程通信。稳定性强,双向全双工。
注意:bind()函数调用成功,会创建一个socket,因此为保证bind成功,通常我们在bind之前,可以使用unlink(“serv.socket”);本地套接字中的客户端不能依赖隐式绑定,需要自己创建一个socke文件后绑定。
本地套接字的服务端实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #define SERV_ADDR "serv.socket" int main () { int lfd = Socket(AF_UNIX,SOCK_STREAM,0 ); struct sockaddr_un servaddr ; bzero(&servaddr,sizeof (servaddr)); servaddr.sun_family = AF_UNIX; strcpy (servaddr.sun_path,SERV_ADDR); int len = offsetof(struct sockaddr_un,sun_path) + strlen (servaddr.sun_path); unlink(SERV_ADDR); Bind(lfd,(struct sockaddr *)&servaddr,len); Listen(lfd,20 ); printf ("accept .....\n" ); struct sockaddr_un cliaddr ; int size; int i; char buf[1024 ]; while (1 ){ len = sizeof (cliaddr); int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,(socklen_t *)&len); len -= offsetof(struct sockaddr_un,sun_path); cliaddr.sun_path[len] = '\0' ; printf ("client bind filename %s\n" ,cliaddr.sun_path); while ((size = read(cfd,buf,sizeof (buf))) > 0 ){ for (i = 0 ; i < size; i++){ buf[i] = toupper (buf[i]); } write(cfd,buf,size); } close(cfd); } close(lfd); return 0 ; }
本地套接字的客户端实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #define SERV_ADDR "serv.socket" #define CLIE_ADDR "clie.socket" int main () { int cfd = Socket(AF_UNIX,SOCK_STREAM,0 ); struct sockaddr_un cliaddr ; bzero(&cliaddr,sizeof (cliaddr)); cliaddr.sun_family = AF_UNIX; strcpy (cliaddr.sun_path,CLIE_ADDR); int len = offsetof(struct sockaddr_un,sun_path) + strlen (cliaddr.sun_path); unlink(CLIE_ADDR); Bind(cfd,(struct sockaddr *)&cliaddr,len); struct sockaddr_un servaddr ; bzero(&servaddr,sizeof (servaddr)); servaddr.sun_family = AF_UNIX; strcpy (servaddr.sun_path,SERV_ADDR); len = offsetof(struct sockaddr_un,sun_path) + strlen (servaddr.sun_path); Connect(cfd, (struct sockaddr *)&servaddr,len); char buf[1024 ]; while (fgets(buf,sizeof (buf),stdin ) != NULL ){ write(cfd,buf,strlen (buf)); len = read(cfd,buf,sizeof (buf)); write(STDOUT_FILENO,buf,len); } close(cfd); return 0 ; }