C++ 高并发服务器
4、高并发服务器
1)多进程并发服务器
大致思路:
1. Socket(); 创建 监听套接字 lfd
2. Bind(); 绑定地址结构 Struct scokaddr_in addr;
3. Listen();
4.while(1){
cfd = Accpet();
pid = fork();
if(pid == 0) { 子进程 read(cfd) --- 小-》大 ---
write(cfd)
close(lfd); 关闭用于连接的套接字 lfd
read();
小---大
write();
}else if(pid > 0){
close(cfd); 关闭用于通信的套接字 cfd
contiue;
}
}
5.子进程:
close(lfd);
read();
小---大
write();
父进程:
close(cfd);
注册信号捕捉函数: SIGCHLD
在回调函数中,完成子进程回收
while(waitpid());
使用多进程并发服务器时要考虑以下几点:
-
父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
-
系统内创建进程个数(与内存大小相关)
-
进程创建过多是否降低整体服务性能(进程调度)
多进程并发服务器实现:
wrap.h wrap.c 为错误封装函数
①wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
void sys_err(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif
②wrap.c
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>
#include "wrap.h"
void sys_err(const char *s)
{
perror(s);
exit(1);
}
int Socket(int family, int type, int protocol)
{
int n;
if ( (n = socket(family, type, protocol)) < 0)
sys_err("socket error");
return n;
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ( (n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
sys_err("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
sys_err("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = connect(fd, sa, salen)) < 0)
sys_err("connect error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
sys_err("listen error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
sys_err("close error");
return n;
}
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}
③server.c
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 9527
void catch_child(int signum)
{
while((waitpid(0, NULL, WNOHANG)) > 0);
return ;
}
int main(int argc, char *argv[])
{
//创建socket
int lfd = 0;
lfd = Socket(AF_INET, SOCK_STREAM, 0);
//bind绑定ip和端口
struct sockaddr_in serv_addr;
//memset(&srv_addr, 0, sizeof(srv_addr)); //将地址结构清零
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
//设置监听个数
Listen(lfd, 128);
//accept
struct sockaddr_in clit_addr;
socklen_t clit_addr_len = sizeof(clit_addr); //创建客户端地址
int cfd = 0;
while(1){
cfd = Accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);
int ret;
pid_t pid;
pid = fork();
if(pid < 0){
sys_err("fork error");
}else if(pid == 0){
close(lfd);
break;
}else{
struct sigaction act; //设置信号捕捉
act.sa_handler = catch_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
if(ret != 0){
sys_err("sigaction error");
}
close(cfd);
continue;
}
}
char buf[MAXLINE];
int ret, i, pid;
if(pid == 0){
for(;;){
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;
}
④client.c
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 9527
int main(int argc, char *argv[])
{
int cfd, ret;
cfd = Socket(AF_INET, SOCK_STREAM, 0);
//创建服务器地址结构
struct sockaddr_in serv_addr; //服务器地址结构
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT); //设置端口
//inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
inet_pton(AF_INET, "IP地址", &serv_addr.sin_addr.s_addr);
//连接服务端
Connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
//与服务端通信逻辑
char buf[MAXLINE];
int n;
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(cfd, buf, strlen(buf));
n = Read(cfd, buf, MAXLINE);
if (n == 0) {
printf("the other side has been closed.\n");
break;
} else
Write(STDOUT_FILENO, buf, n);
}
}
2)多线程并发服务器
大致思路:
1. `Socket();` 创建 监听套接字 lfd
2. `Bind();` 绑定地址结构 `Struct scokaddr_in addr;`
3. `Listen();`
4.while(1){
cfd = Accept(lfd, )
pthread_create(&tid, NULL, tfn, NULL);
pthread_detach(tid); //pthread_join(tid, void **);新线程--专用于回收子线程
}
5.子线程:
void *tfn(void *arg)
{
close(lfd);
read(cfd);
小---大
write(cfd);
}
多进程并发服务器实现:
添加wrap.h wrap.c 为错误封装函数,内容同上
①server.c
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
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[MAXLINE];
char str[INET_ADDRSTRLEN];
/* 可以在创建线程前设置线程创建属性,设为分离态,哪种效率高内? */
pthread_detach(pthread_self());
while (1) {
n = Read(ts->connfd, buf, MAXLINE);
if (n == 0) {
printf("the other side has been closed.\n");
break;
}
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
ntohs((*ts).cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
Write(ts->connfd, buf, n);
}
Close(ts->connfd);
}
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
int i = 0;
pthread_t tid;
struct s_info ts[256];
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
ts[i].cliaddr = cliaddr;
ts[i].connfd = connfd;
/* 达到线程最大数时,pthread_create出错处理, 增加服务器稳定性 */
pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
i++;
}
return 0;
}
②client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
3)多路I/O转接服务器
原理:
借助内核,select 来监听。客户端连接、数据通信事件
① select 函数
#include<sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *excepfds, struct timeval *timeout);
nfds: 监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合,传入传出参数
writefds: 写 文件描述符监听集合,传入传出参数
exceptfds: 异常 文件描述符监听集合,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
返回值:
1. >0 :所有监听集合(3个)中,满足对应事件的总数
2. 0 :没有满足监听条件的文件描述符
3. -1 :error
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
/*把文件描述符集合里所有位清0(清空一个文件描述符集合)*/
void FD_ZERO(fd_set *set);
fd_set rset;
FD_ZERO(&rset);
/*把文件描述符集合里fd位置1(将待监听的文件描述符添加到监听集合)*/
void FD_SET(int fd, fd_set *set);
FD_SET(3, &rset); FD_SET(4, &rset); FD_SET(5, &rset);
/*把文件描述符集合里fd清0(将一个文件描述符从集合中移除)*/
void FD_CLR(int fd, fd_set *set);
FD_CLR(4, &rset);
/*测试文件描述符集合里fd是否置1(判断一个文件描述符是否在监听集合中)*/
int FD_ISSET(int fd, fd_set *set);
返回值:在->1 不在->0
FD_ISSET(4, &rset)
思路分析:
-
lfd = socket();
创建套接字 -
bind();
绑定地址结构 -
listen();
设置监听上限 -
fd_set rset, allset;
创建r监听集合 -
FD_ZERO(&allset);
将r监听集合清空 -
FD_SET(lfd, &allset)
将 lfd 添加到读集合 -
while(1){ rset = allset; //保存监听集合 ret = select(lfd+1, &rset, NULL, NULL, NULL); //监听文件描述符集合对应事件 if(ret > 0){ //有监听的描述符满足对应事件 if(FD_ISSET(lfd, &rset)){ //1 在。 0 不在 cfd = accept(); //建立连接,返回用于通信的文件描述符 FD_SET(cfd, &allset); //添加到通信描述符集合中 } for(i = lfd + 1; i < Max文件描述符; i++){ FD_ISSET(i, &rset); //有read,write事件 read(); 小——大 write(); } } }
实现代码:
同样要包含上述错误处理函数文件wrap.h wrap.c
server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int listenfd, connfd;
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serv_addr, sizeof(serv_addr)); //将地址结构清零
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
fd_set rset, allset; //定义 读集合,备份集合allset
int ret, maxfd = 0, n, i, j;
char buf[BUFSIZ];
maxfd = listenfd; //最大文件描述符
FD_ZERO(&allset); //清空 监听集合
FD_SET(listenfd, &allset); //将待监听fd添加到监听集合
while(1){
rset = allset; //备份
ret = select(maxfd + 1, &rset, NULL, NULL, NULL); //使用selcet监听
if(ret < 0){
sys_err("select error");
}
if(FD_ISSET(listenfd, &rset)){
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);//建立连接 ---不会阻塞
FD_SET(connfd, &allset); //将新生产的fd,添加到监听集合中,监听数据读事件
if(maxfd < connfd) //修改maxfd
maxfd = connfd;
if(ret == 1) //说明select 只返回一个,并且是listenfd,后续执行无需执行
continue;
}
for(i = listenfd + 1; i <= maxfd; i++){ //处理满足读事件的 fd
if(FD_ISSET(i, &rset)){ //找到满足读事件的那个 fd
n = read(i, buf, sizeof(buf));
if(n == 0){ //检测到客户端已经关闭连接
Close(i);
FD_CLR(i, &allset); //将关闭的fd,移除监听集合
}else if(n == -1){
sys_err("read error");
}
for(j = 0; j < n; j++){
buf[j] = toupper(buf[j]);
write(i, buf, n);
write(STDOUT_FILENO, buf, n);
}
}
}
}
Close(listenfd);
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
select 优缺点
缺点:
- 监听上限受文件描述符限制。最大 1024 。
- 检测满足条件的fd,自己添加业务逻辑提高小。提高了编码难度
select是轮询方式,可以使用自定义数组,防止遍历1024个文件描述符,指定需要监听的cfd。
优点:
- 服务端接入少
- 跨平台。win、Linux、macOS、类Unix、mips
基于select的I/O复用技术速度慢的原因
- 调用select函数后常见的针对所有文件描述符的循环语句(轮询方式)
- 每次调用select函数时都需要向该函数传递监视对象信息
② poll 函数
相对于select改进了一点点,但很少用
函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfs_t nfds, int timeout);
fds:监听的文件描述符[数组]
struct pollfd {
int fd : 待监听的文件描述符
short events : 待监听的文件描述符对应的监听事件
取值:POLLIN、POLLOUT、POLLERR
short revents: 传入时给0。如果满足对应事件的话,返回 非0->POLLIN、POLLOUT、POLLERR
}
nfds:监听数组的实际有效监听个数
timeout: >0 : 超时时长。单位:毫秒
-1 : 阻塞等待
0 :不阻塞
返回值:返回满足监听事件的文件描述符总个数
poll 优缺点
优点:
- 自带数组结构。可以将 监听事件集合 和 返回时间集合 分离
- 拓展 监听上线。超出1024 限制
缺点:
- 不能跨平台。Linux
- 无法直接定位满足监听事件的文件描述符,编码难度大
突破1024文件描述符限制:
cat /proc/sys/fs/file-max //当前计算机所能打开的最大文件个数。受硬件影响
ulimit -a //当前用户下的进程,默认打开文件描述符个数,缺省1024
修改:
打开 sudo vi/etc/security/limits.conf 写入
* soft nofile 65536 //设置默认值,可以直接借助命令修改[注销用户,使其生效]
* hard nofile 100000 //命令修改上限
//复习read函数返回值:
>0 :实际读到的字节数
=0 :socket中,表示对端关闭。close()
-1 : 如果 errno == EINTR 被异常终止,需要重新启动
如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据,需要再次读
如果 errno == ECONNRESET 说明连接被重置。需要 close(),移除监听队列
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024
int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
client[0].fd = listenfd;
client[0].events = POLLRDNORM; /* listenfd监听普通读事件 */
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
maxi = 0; /* client[]数组有效元素中最大元素下标 */
for ( ; ; ) {
nready = poll(client, maxi+1, -1); /* 阻塞 */
if (client[0].revents & POLLRDNORM) { /* 有客户端链接请求 */
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, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 1; i < OPEN_MAX; i++) {
if (client[i].fd < 0) {
client[i].fd = connfd; /* 找到client[]中空闲的位置,存放accept返回的connfd */
break;
}
}
if (i == OPEN_MAX)
sys_err("too many clients");
client[i].events = POLLRDNORM; /* 设置刚刚返回的connfd,监控读事件 */
if (i > maxi)
maxi = i; /* 更新client[]中最大元素下标 */
if (--nready <= 0)
continue; /* 没有更多就绪事件时,继续回到poll阻塞 */
}
for (i = 1; i <= maxi; i++) { /* 检测client[] */
if ((sockfd = client[i].fd) < 0)
continue;
if (client[i].revents & (POLLRDNORM | POLLERR)) {
if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) { /* 当收到 RST标志时 */
/* connection reset by client */
printf("client[%d] aborted connection\n", i);
Close(sockfd);
client[i].fd = -1;
} else {
sys_err("read error");
}
} else if (n == 0) {
/* connection closed by client */
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]);
Writen(sockfd, buf, n);
}
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
③epoll 函数
epoll是Linux下多路复用IO接口select/poll的增强版本
目前epoll是Linux大规模并发网络程序中的热门首选模型
epoll函数刚好能够克服select函数不合理的地方。
epoll 优点(高效,能突破1024文件描述符)
- 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句
- 调用对应于select函数的epoll_wait函数时无需每次传递监视信息
epoll 缺点
- 不能跨平台
突破1024上限同poll
Epoll相对select/poll的优势:
Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大 ,具体数目可以 cat /proc/sys/fs/file-max[599534] ,并且现在服务器的内存都很大,所以这个不是问题。
效率提升,epoll对于句柄事件的选择不是遍历的,是事件响应的,就是句柄上事件来就马上选择出来,不需要遍历整个句柄链表,因此效率非常高,内核将句柄用红黑树保存的,IO效率不随FD数目增加而线性下降。
内存拷贝, select让内核把 FD 消息通知给用户空间的时候使用了内存拷贝的方式,开销较大,但是Epoll 在这点上使用了共享内存的方式,这个内存拷贝也省略了。
epoll服务器端实现需要的三个函数
epoll_create:创建保存epoll文件描述符的空间
epoll_ctl:向空间注册并注销文件描述符
epoll_wait:与select类似,等待文件描述符发生变化
头文件:#include<sys/epoll.h>
epoll_create 函数:
调用epoll_create函数时创建的文件描述符保存空间叫做“epoll例程”,有些情况名称会有所不同。size并非用来决定epoll例程的大小,而仅供操作系统参考
//创建epoll例程(句柄)——文件描述符保存空间
int epoll_create(int size);
size:创建的红黑树的监听节点数量(仅供内核参考)。
返回值:指向新创建的红黑树的根节点的 fd(epoll文件描述符)
失败:-1 error
epoll_ctl 函数
生成例程后,应在其内部注册监视对象文件描述符,此时使用epoll函数
//操作监听红黑树(控制某个epoll监控的文件描述符上的事件:注册、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:指向新创建的红黑树的根节点的 fd (epoll文件描述符)
op:对该监听红黑树所作的操作
EPOLL_CTL_ADD 添加fd 到监听红黑树
EPOLL_CTL_MOD 修改fd 在监听红黑树上的监听事件
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
fd :待监听的fd
event:
本质 struct epoll_event 结构体、地址
成员events: EPOLLIN / EPOLLOUT / EPOLLERR
成员data 联合体:
int fd; //对应监听事件的fd
void* ptr;
struct evt{ //使ptr自动回调(反应堆)
int fd;
void(*func)(int fd);
}*ptr
unit32_t u32;
unit64_t u64;
//返回值:成功 0; 失败:-1 error
/*EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里*/
例:
epoll_ctl(A, EPOLL_CTL_ADD, B, C)
表示:“epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件”
epoll_wait 函数
//等待所监控文件描述符上有事件的产生,类似于select()调用。
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);
epfd:指向新创建的红黑树的根节点的 fd (epoll文件描述符)
events:传出参数,[简单看作一个数组],满足监听条件的那些 fd 结构体
maxevents:数组 元素的总个数 1024
struct epoll_event events[1024]
timeout:
-1:阻塞
0:不阻塞
>0:超时时间(毫秒)
返回值:
>0:满足监听的总个数。可以用作循环上限
0:没有fd满足监听事件
-1:失败 error
epoll实现多路IO转接思路:
-
lfd = socket();
创建socket,得到监听文件描述符 -
setsockopt();
设置端口复用 -
bind();
绑定 -
listen();
监听 -
int epfd = epoll_create(1024); //epfd:创建一棵epoll树(红黑树)
-
//tep:用来设置单个fd属性,ep是epoll_wait()传出的满足监听事件的数组 struct epoll_event teo, ep[1024]; tep.events = EPOLLIN; tep.data.fd = lfd; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); //将lfd添加到监听红黑树上
-
while(1){ ret = epoll_wait(epfd, ep, 1024, -1); //实施监听 for(i = 0; i < ret; i++){ if(ep[i].data.fd == lfd){ //lfd满足读事件,有新的客户端发起连接请求 cfd = Accept(); tep.events = EPOLLIN; //初始化 cfd的属性 epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep); }else{ //cfd们 满足读事件, 有客户端写数据来 n = read(ep[i].data.fd, buf, sizeof(buf)); if(n == 0){ close(ep[i].data.fd); //将关闭的cfd,从监听树上摘下 epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd, NULL); }else if(n > 0){ 小——大 write(ep[i].data.fd, buf, n); } } } }
添加wrap.h wrap.c 为错误封装函数,内容同上
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024
int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
int nready, efd, res;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
struct epoll_event tep, ep[OPEN_MAX];
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
Listen(listenfd, 20);
for (i = 0; i < OPEN_MAX; i++)
client[i] = -1;
maxi = -1;
efd = epoll_create(OPEN_MAX);
if (efd == -1)
perr_exit("epoll_create");
tep.events = EPOLLIN; tep.data.fd = listenfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
if (res == -1)
perr_exit("epoll_ctl");
while (1) {
nready = epoll_wait(efd, ep, OPEN_MAX, -1); /* 阻塞监听 */
if (nready == -1)
perr_exit("epoll_wait");
for (i = 0; i < nready; i++) {
if (!(ep[i].events & EPOLLIN))
continue;
if (ep[i].data.fd == listenfd) {
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, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (j = 0; j < OPEN_MAX; j++) {
if (client[j] < 0) {
client[j] = connfd; /* save descriptor */
break;
}
}
if (j == OPEN_MAX)
perr_exit("too many clients");
if (j > maxi)
maxi = j; /* max index in client[] array */
tep.events = EPOLLIN;
tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if (res == -1)
perr_exit("epoll_ctl");
} else {
sockfd = ep[i].data.fd;
n = Read(sockfd, buf, MAXLINE);
if (n == 0) {
for (j = 0; j <= maxi; j++) {
if (client[j] == sockfd) {
client[j] = -1;
break;
}
}
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if (res == -1)
perr_exit("epoll_ctl");
Close(sockfd);
printf("client[%d] closed connection\n", j);
} else {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Writen(sockfd, buf, n);
}
}
}
}
close(listenfd);
close(efd);
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
4)epol 进阶
epoll除了提供select/poll 那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,还提高应用程序效率。
epoll事件有两种模型:
Edge Triggered(ET)
边缘触发只有数据到来才会触发,不管缓冲区是否还有数据Level Triggered(LT)
水平触发只要有数据都会触发
思考如下步骤:
- 假定我们已经把一个用来管道中读取数据的文件描述符(rfd)添加到epoll描述符
- 管道的另一端写入了2KB的数据
- 调用epoll_wait,并且它会返回rfd,说明它已经准备好读取操作
- 读取1KB数据
- 调用epoll_wait……
在这个过程中,有两种工作模式:
① ET 模式
ET 模式(Edge Triggere):边沿触发
缓冲区剩余未读尽的数据不会导致
epoll_wait
返回。新的读写事件满足才会触发。ET模式是一种高效模式,但只支持 非阻塞模式。非阻塞模式就要用忙轮询
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flg);
② LT 模式
LT 模式(Level Triggered):水平触发——默认采用方式
缓冲区剩余未读尽的数据会导致epoll_wait返回。
与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。
示例:
event.events = EPOLLIN | EPOLLET;
//ET 边沿触发
event.events = EPOLLIN;
//LT 水平触发
【epoll为什么要有EPOLLET触发模式?】:
如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边缘触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
实例一:
基于管道epoll ET触发模式
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h>
#define MAXLINE 10
int main(int argc, char *argv[])
{
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(2);
}
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.events = EPOLLIN | EPOLLET; /* ET 边沿触发 ,默认是水平触发 */
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;
}
实例二:
基于网络C/S模型的epoll ET触发模式
server.c
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#define MAXLINE 10
#define SERV_PORT 8080
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, efd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
struct epoll_event event;
struct epoll_event resevent[10];
int res, len;
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; /* ET 边沿触发 ,默认是水平触发 */
printf("Accepting connections ...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
while (1) {
res = epoll_wait(efd, resevent, 10, -1);
printf("res %d\n", res);
if (resevent[0].data.fd == connfd) {
len = read(connfd, buf, MAXLINE/2);
write(STDOUT_FILENO, buf, len);
}
}
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define MAXLINE 10
#define SERV_PORT 8080
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, i;
char ch = 'a';
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
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(sockfd, buf, sizeof(buf));
sleep(10);
}
Close(sockfd);
return 0;
}
实例三:
基于网络C/S非阻塞模型的epoll ET触发模式
server.c
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#define MAXLINE 10
#define SERV_PORT 8080
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, efd, flag;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
struct epoll_event event;
struct epoll_event resevent[10];
int res, len;
efd = epoll_create(10);
/* event.events = EPOLLIN; */
event.events = EPOLLIN | EPOLLET; /* ET 边沿触发 ,默认是水平触发 */
printf("Accepting connections ...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
flag = fcntl(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
while (1) {
printf("epoll_wait begin\n");
res = epoll_wait(efd, resevent, 10, -1);
printf("epoll_wait end res %d\n", res);
if (resevent[0].data.fd == connfd) {
while ((len = read(connfd, buf, MAXLINE/2)) > 0)
write(STDOUT_FILENO, buf, len);
}
}
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define MAXLINE 10
#define SERV_PORT 8080
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, i;
char ch = 'a';
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
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(sockfd, buf, sizeof(buf));
sleep(10);
}
Close(sockfd);
return 0;
}
③ epoll反应堆
epoll_create(); // 创建监听红黑树
epoll_ctl(); // 向书上添加监听fd
epoll_wait(); // 监听
有监听fd事件发送--->返回监听满足数组--->判断返回数组元素--->
lfd满足accept--->返回cfd---->read()读数据--->write()给客户端回应。
epoll_create(); // 创建监听红黑树
epoll_ctl(); // 向书上添加监听fd
epoll_wait(); // 监听
有客户端连接上来--->lfd调用acceptconn()--->将cfd挂载到红黑树上监听其读事件--->
epoll_wait()返回cfd--->cfd回调recvdata()--->将cfd摘下来监听写事件--->
epoll_wait()返回cfd--->cfd回调senddata()--->将cfd摘下来监听读事件--->...--->
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define MAX_EVENTS 1024 /*监听上限*/
#define BUFLEN 4096 /*缓存区大小*/
#define SERV_PORT 6666 /*端口号*/
void recvdata(int fd,int events,void *arg);
void senddata(int fd,int events,void *arg);
/*描述就绪文件描述符的相关信息*/
struct myevent_s
{
int fd; //要监听的文件描述符
int events; //对应的监听事件,EPOLLIN和EPLLOUT
void *arg; //指向自己结构体指针
void (*call_back)(int fd,int events,void *arg); //回调函数
int status; //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
char buf[BUFLEN];
int len;
long last_active; //记录每次加入红黑树 g_efd 的时间值
};
int g_efd; //全局变量,作为红黑树根
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组. +1-->listen fd
/*
* 封装一个自定义事件,包括fd,这个fd的回调函数,还有一个额外的参数项
* 注意:在封装这个事件的时候,为这个事件指明了回调函数,一般来说,一个fd只对一个特定的事件
* 感兴趣,当这个事件发生的时候,就调用这个回调函数
*/
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int fd,int events,void *arg), void *arg)
{
ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
if(ev->len <= 0)
{
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
}
ev->last_active = time(NULL); //调用eventset函数的时间
return;
}
/* 向 epoll监听的红黑树 添加一个文件描述符 */
void eventadd(int efd, int events, struct myevent_s *ev)
{
struct epoll_event epv={0, {0}};
int op = 0;
epv.data.ptr = ev; // ptr指向一个结构体(之前的epoll模型红黑树上挂载的是文件描述符cfd和lfd,现在是ptr指针)
epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
if(ev->status == 0) //status 说明文件描述符是否在红黑树上 0不在,1 在
{
op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1
ev->status = 1;
}
if(epoll_ctl(efd, op, ev->fd, &epv) < 0) // 添加一个节点
printf("event add failed [fd=%d],events[%d]\n", ev->fd, events);
else
printf("event add OK [fd=%d],events[%0X]\n", ev->fd, events);
return;
}
/* 从epoll 监听的 红黑树中删除一个文件描述符*/
void eventdel(int efd,struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
if(ev->status != 1) //如果fd没有添加到监听树上,就不用删除,直接返回
return;
epv.data.ptr = NULL;
ev->status = 0;
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);
return;
}
/* 当有文件描述符就绪, epoll返回, 调用该函数与客户端建立链接 */
void acceptconn(int lfd,int events,void *arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1)
{
if(errno != EAGAIN && errno != EINTR)
{
sleep(1);
}
printf("%s:accept,%s\n",__func__, strerror(errno));
return;
}
do
{
for(i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素,类似于select中找值为-1的元素
{
if(g_events[i].status ==0)
break;
}
if(i == MAX_EVENTS) // 超出连接数上限
{
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break;
}
int flag = 0;
if((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) //将cfd也设置为非阻塞
{
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
eventset(&g_events[i], cfd, recvdata, &g_events[i]); //找到合适的节点之后,将其添加到监听树中,并监听读事件
eventadd(g_efd, EPOLLIN, &g_events[i]);
}while(0);
printf("new connect[%s:%d],[time:%ld],pos[%d]",inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return;
}
/*读取客户端发过来的数据的函数*/
void recvdata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = recv(fd, ev->buf, sizeof(ev->buf), 0); //读取客户端发过来的数据
eventdel(g_efd, ev); //将该节点从红黑树上摘除
if (len > 0)
{
ev->len = len;
ev->buf[len] = '\0'; //手动添加字符串结束标记
printf("C[%d]:%s\n", fd, ev->buf);
eventset(ev, fd, senddata, ev); //设置该fd对应的回调函数为senddata
eventadd(g_efd, EPOLLOUT, ev); //将fd加入红黑树g_efd中,监听其写事件
}
else if (len == 0)
{
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
}
else
{
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return;
}
/*发送给客户端数据*/
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0); //直接将数据回射给客户端
eventdel(g_efd, ev); //从红黑树g_efd中移除
if (len > 0)
{
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventset(ev, fd, recvdata, ev); //将该fd的回调函数改为recvdata
eventadd(g_efd, EPOLLIN, ev); //重新添加到红黑树上,设为监听读事件
}
else
{
close(ev->fd); //关闭链接
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
}
/*创建 socket, 初始化lfd */
void initlistensocket(int efd, short port)
{
struct sockaddr_in sin;
int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞
memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
listen(lfd, 20);
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);
/* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); //将lfd添加到监听树上,监听读事件
return;
}
int main()
{
int port=SERV_PORT;
g_efd = epoll_create(MAX_EVENTS + 1); //创建红黑树,返回给全局 g_efd
if(g_efd <= 0)
printf("create efd in %s err %s\n", __func__, strerror(errno));
initlistensocket(g_efd, port); //初始化监听socket
struct epoll_event events[MAX_EVENTS + 1]; //定义这个结构体数组,用来接收epoll_wait传出的满足监听事件的fd结构体
printf("server running:port[%d]\n", port);
int checkpos = 0;
int i;
while(1)
{
/* long now = time(NULL);
for(i=0; i < 100; i++, checkpos++)
{
if(checkpos == MAX_EVENTS);
checkpos = 0;
if(g_events[checkpos].status != 1)
continue;
long duration = now -g_events[checkpos].last_active;
if(duration >= 60)
{
close(g_events[checkpos].fd);
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]);
}
} */
//调用eppoll_wait等待接入的客户端事件,epoll_wait传出的是满足监听条件的那些fd的struct epoll_event类型
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0)
{
printf("epoll_wait error, exit\n");
exit(-1);
}
for(i = 0; i < nfd; i++)
{
//evtAdd()函数中,添加到监听树中监听事件的时候将myevents_t结构体类型给了ptr指针
//这里epoll_wait返回的时候,同样会返回对应fd的myevents_t类型的指针
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
//如果监听的是读事件,并返回的是读事件
if((events[i].events & EPOLLIN) &&(ev->events & EPOLLIN))
{
ev->call_back(ev->fd, events[i].events, ev->arg);
}
//如果监听的是写事件,并返回的是写事件
if((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT))
{
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
return 0;
}
④ 线程池并发服务器
线程池,模块分析:
-
main()
- 创建线程池
- 向线程池中添加任务,借助回调处理任务
- 销毁线程池
-
pthreadpoll_create()
- 创建线程池结构体 指针
- 初始化线程池结构体(N个成员变量)
- 创建N个任务线程
- 创建1个管理者线程
- 失败时,销毁开辟的所有空间(释放)
-
threadpoll_thread()
- 进入子线程回调函数
- 接收参数
void *arg
--> poll结构体 - 加锁 --> lock --> 整个结构体锁
- 判断条件变量 --> wait
-
adjust_thread()
- 进入管理者线程回调函数
- 接收参数
void *arg
--> poll结构体 - 加锁 --> lock --> 整个结构体锁
- 获取管理线程池要用到的变量。
task_num,live_num,busy_num
- 根据既定算法,使用上述3变量,判断是否应该创建、销毁线程池中 指定步长的线程
-
threadpoll_add()
-
总功能
-
模拟产生任务 num[20]
-
设置回调函数,处理任务 sleep(1)代表处理完成
-
-
内部实现
- 初始化 任务队列结构体成员。 回调函数
function, arg
- 利用环形队列机制,实现添加任务。借助队尾指针挪移%实现
- 唤醒阻塞在 条件变量上的线程
- 解锁
- 初始化 任务队列结构体成员。 回调函数
-
-
从3. 中的wait之后继续执行,处理任务
- 加锁
- 获取 任务处理回调函数,及参数
- 利用环形队列机制,实现处理任务。借助队头指针挪移%实现
- 唤醒阻塞在 条件变量 上的 server
- 解锁
- 加锁
- 改忙线程数++
- 解锁
- 执行处理任务的线程
- 加锁
- 改忙线程数--
- 解锁
-
创建 销毁线程
-
管理者线程根据
task_num,live_num,busy_num
-
根据既定算法,使用上述3变量,判断是否应该创建、销毁线程池中 指定步长的线程
-
如果满足 创建条件
pthread_create();
回调 任务线程函数live_num++
-
如果满足 销毁条件
signal
给 阻塞在条件变量上的线程 发送 假条件满足信号- 跳转至 --170 wait阻塞线程会被 假信号 唤醒。 判断:
wait_exit_thr_num > 0 pthread_exit();
-
⑤ UDP 服务器
TCP通信和UDP通信各自的缺点:
-
TCP:面向连接的,可靠数据包传输。 对于不稳定的网络层,采用完全弥补的通信方式。丢包重传。
- 优点:稳定
- 数据流量稳定、速度稳定、顺序 稳定
- 缺点:
- 传输速度慢,效率低,开销大
- 使用场景:
- 数据完整性要求较高,不追求效率
- 大数据传输、文件传输
- 优点:稳定
-
UDP:无连接的,不可靠的数据报传递。 对于不稳定的网络层,采用完全不弥补的通信方式。默认还原网络状况
- 优点:
- 传输速度快,效率高,开销小
- 缺点:不稳定
- 数据流量不稳定、速度不稳定、顺序不稳定
- 使用场景:
- 时效性要求较高的场合、稳定性其次
- 游戏、视频会议、视频电话 腾讯、华为、阿里 --- 应用层数据校验协议,弥补UDP的不足
- 优点:
UDP实现 C/S 模型:
recv()/send() //只能用于TCP通信。代替read,write
accept(); //Connect()被舍弃
server:
lfd = socekt(AF_INET, SOCK_DGRAM, 0); //SOCK_DGRAM--报式协议
bind();
listen(); //可有可无
while(1){
read(cfd, buf, ) //read被替换成recvfrom()--涵盖accept传出地址结构
小--大
write(); //被替换成--sendto();
}
close();
client:
connfd = socket(AF_INET, SOCK_DGRAM, 0);
sendto("服务器地址结构", 地址结构大小);
recvfrom();
写到屏幕
close();
recvfrom 函数
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addr);
参数:
sockfd:套接字
buf:缓冲区地址
len:缓冲区大小
flags:0
src_addr:(struct sockaddr*)&addr 传出。 对端地址结构
addrlen:传入传出
返回值:
成功接收数据字节数。失败:-1 errno 0:对端关闭
sendto 函数
#include<sys/types.h>
#include<sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
sockfd:套接字
buf:存储数据的缓冲区
len:数据长度
flags:0
dest_addr:(struct sockaddr*)&addr 传入。 目标地址结构
addrlen:传入传出
返回值:
成功接收数据字节数。失败:-1 errno
UDP实现的并发服务器和客户端:
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现。
编译运行server,在两个终端里各开一个client与server交互,看看server是否具有并发服务的能力。用Ctrl+C关闭server,然后再运行server,看此时client还能否和server联系上。和前面TCP程序的运行结果相比较,体会无连接的含义。
server.c
#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <ctype.h>
#define MAXLINE 80
#define SERV_PORT 6666
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int sockfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
n = recvfrom(sockfd, buf, MAXLINE,0, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (n == -1)
perror("recvfrom error");
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
if (n == -1)
perror("sendto error");
}
close(sockfd);
return 0;
}
client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <ctype.h>
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
int sockfd, n;
char buf[MAXLINE];
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
while (fgets(buf, MAXLINE, stdin) != NULL) {
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (n == -1)
perror("sendto error");
n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);
if (n == -1)
perror("recvfrom error");
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}
懂得利用TCP socket 代码 重编写UDP socket
⑥ socket IPC(本地套接字)
IPC:pipe、fifo、mmap、信号、本地套(domain)---CS模型
对比网络编程TCP C/S 模型,注意以下几点:
1、int socket(int domain, int type, int protocol)
参数:
domian:AF_INET -> AF_UNIX/AF_LOCAL
type:SOCK_STREAM/SOCK_DGRAM 都可以
2、地址结构:sockaddr_in
--> sockaddr_un
struct sockaddr_in srv_addr;
--> sockaddr_un srv_addr
srv_addr.sin_family = AF_INET;
--> srv_addr.sun_family = AF_UNIX;
srv_addr.sin_port = htons(8888);
--> strcpy(srv_addr.sun_path, "srv.socket")
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
--> len = offsetof(struct sockaddr_un, sun_path)+strlen("srv.socket");
bind(fd, (struct sockaddr *)&arv_addr, sizeof(srv_addr));
--> bind(fd, (struct sockaddr *)&arv_addr, len);
3、bind()函数调用成功,会创建一个socket。因此为保证bind成功,通常我们在bain之前,可以使用unlink("srv.socket");
4、客户端不能依赖“隐式绑定”。并且应该在通信建立过程中,创建且初始化2个结构地址
1)client_addr --> bind()
2)server_addr --> connect()
对比网络套接字地址结构和本地套接字地址结构:
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */ 地址结构类型
__be16 sin_port; /* Port number */ 端口号
struct in_addr sin_addr; /* Internet address */ IP地址
};
struct sockaddr_un {
__kernel_sa_family_t sun_family; /* AF_UNIX */ 地址结构类型
char sun_path[UNIX_PATH_MAX]; /* pathname */ socket文件名(含路径)
};
以下程序将UNIX Domain socket绑定到一个地址。
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
#define offsetof(type, member) ((int)&((type *)0)->MEMBER)
server.c
#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#define QLEN 10
/*
* Create a server endpoint of a connection.
* Returns fd if all OK, <0 on error.
*/
int serv_listen(const char *name)
{
int fd, len, err, rval;
struct sockaddr_un un;
/* create a UNIX domain stream socket */
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return(-1);
/* in case it already exists */
unlink(name);
/* fill in socket address structure */
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
/* bind the name to the descriptor */
if (bind(fd, (struct sockaddr *)&un, len) < 0) {
rval = -2;
goto errout;
}
if (listen(fd, QLEN) < 0) { /* tell kernel we're a server */
rval = -3;
goto errout;
}
return(fd);
errout:
err = errno;
close(fd);
errno = err;
return(rval);
}
int serv_accept(int listenfd, uid_t *uidptr)
{
int clifd, len, err, rval;
time_t staletime;
struct sockaddr_un un;
struct stat statbuf;
len = sizeof(un);
if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0)
return(-1); /* often errno=EINTR, if signal caught */
/* obtain the client's uid from its calling address */
len -= offsetof(struct sockaddr_un, sun_path); /* len of pathname */
un.sun_path[len] = 0; /* null terminate */
if (stat(un.sun_path, &statbuf) < 0) {
rval = -2;
goto errout;
}
if (S_ISSOCK(statbuf.st_mode) == 0) {
rval = -3; /* not a socket */
goto errout;
}
if (uidptr != NULL)
*uidptr = statbuf.st_uid; /* return uid of caller */
/* we're done with pathname now */
unlink(un.sun_path);
return(clifd);
errout:
err = errno;
close(clifd);
errno = err;
return(rval);
}
int main(void)
{
int lfd, cfd, n, i;
uid_t cuid;
char buf[1024];
lfd = serv_listen("foo.socket");
if (lfd < 0) {
switch (lfd) {
case -3:perror("listen"); break;
case -2:perror("bind"); break;
case -1:perror("socket"); break;
}
exit(-1);
}
cfd = serv_accept(lfd, &cuid);
if (cfd < 0) {
switch (cfd) {
case -3:perror("not a socket"); break;
case -2:perror("a bad filename"); break;
case -1:perror("accept"); break;
}
exit(-1);
}
while (1) {
r_again:
n = read(cfd, buf, 1024);
if (n == -1) {
if (errno == EINTR)
goto r_again;
}
else if (n == 0) {
printf("the other side has been closed.\n");
break;
}
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, n);
}
close(cfd);
close(lfd);
return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define CLI_PATH "/var/tmp/" /* +5 for pid = 14 chars */
/*
* Create a client endpoint and connect to a server.
* Returns fd if all OK, <0 on error.
*/
int cli_conn(const char *name)
{
int fd, len, err, rval;
struct sockaddr_un un;
/* create a UNIX domain stream socket */
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return(-1);
/* fill socket address structure with our address */
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
/* in case it already exists */
unlink(un.sun_path);
if (bind(fd, (struct sockaddr *)&un, len) < 0) {
rval = -2;
goto errout;
}
/* fill socket address structure with server's address */
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
if (connect(fd, (struct sockaddr *)&un, len) < 0) {
rval = -4;
goto errout;
}
return(fd);
errout:
err = errno;
close(fd);
errno = err;
return(rval);
}
int main(void)
{
int fd, n;
char buf[1024];
fd = cli_conn("foo.socket");
if (fd < 0) {
switch (fd) {
case -4:perror("connect"); break;
case -3:perror("listen"); break;
case -2:perror("bind"); break;
case -1:perror("socket"); break;
}
exit(-1);
}
while (fgets(buf, sizeof(buf), stdin) != NULL) {
write(fd, buf, strlen(buf));
n = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, n);
}
close(fd);
return 0;
}
本地套接字和网络套接字实现对比:
- 通信对象:
- 本地套接字是让单机中的进程进行通信;
- 网络套接字是让网络中的进程进行通信;
- 通信方式:
- 网络套接字是通过绑定ip和端口;
- 本地套接字是绑定套接字文件的路径名。
除了以上两点外,基本相同。