EPOLL高并发服务器


前言

提示:这里可以添加本文要记录的大概内容:

学习写一个简单的高并发服务器,阐述 每个函数的参数的功能与作用。

预备知识

网络字节序:

概念: 大端法: 高位存在低地址, 地位存在高地址
		
		小端法: 高位存在高地址, 低位存在低地址
	在TCP/IP 协议规定, 网络数据流对应采用大端字节序

网络字节序和主机字节序的转换

#include<arpa/inet,h>

uint32_t htonl(uint32_t hostlong);  //本地转网络(ip地址)
uint16_t htons(uint16_t hostshort); //本地端口 转 网络端口
uint32_t ntohl(uint32_t netlong); //网络ip 转本地ip
uint16_t ntohs(uint16_t netshort); //网络端口 转 本地端口

如果主机是小端字节序, 这些函数将参数做相应的大小端转换然后返回, 如果主机是大端字节序, 这些函数不做转换, 将参数原封不动的返回

IP地址转换函数

支持 ipv4 的in_addr, 与 ipv6 in6_addr;

#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src,char *dst,socklen_t size);

sockaddr 数据结构

sockaddr 数据结构

struct sockaddr{
	sa_familu_t sa_family; 
	char sa_data[14];
};

sockaddr_in 结构体

struct sockaddr_in{
	_kernel_sa_familu_t sin_family; /*地址结构类型*/
	_be16 sin_port; //端口号
	struct inaddr sin_addr; //ip地址
	/*Pad to size of 'struct sockaddr'.*/
	unsigned char _pad[__SOCK_SIZE__ - sizeof(short int] -
	sizeof(unsigned short int )-sizeof(struct in_addr);
};
struct in_addr{ //Internet address.
	_be32 s_addr; 
};

一、socket模型创建流程图

在这里插入图片描述

socket 函数

#include <sys/types.h> /*see notes*/
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain:

AF_INET 这是大多数用来产生socket 的协议,使用TCP或UDP来传输, 用IPV4的地址
AF_INET6 用IPV6的地址生产socket 协议
AF_UNIX 本地协议,使用在Unix 和Linux 系统上, 一般都是当客户端和服务器在同一台机器上得到时候使用

type:

SOCK_STREAM 这个协议是按照顺序的, 可靠的,数据完整的基于字节流的链接.这是一个使用最多的socket 类型, 这个socket是使用TCP进行传输
SOCK_GREAM 这个协议是无连接的, 固定长度的传输调用.该协议是不可靠的,使用UDP来进行它的链接

protocol:

传0表示使用默认协议

返回值:

成功:返回指向新创建的socket 的文件描述符;
失败: 返回-1,设置 errno

bind函数

#include<sys/type.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//其中第三个参数:
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr))'
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //任意IP地址
servaddr.sin_port=htons(6666);

sockfd:

socket 文件描述符

addr:

构造出IP地址加端口号

addrlen:

sizeof(addr) 长度

返回值:

成功返回0; 失败返回-1, 设置errno

listen 函数

#include<sys/type.h>
#include<sys/socket.h>
int listen(int sockfd.int backlog);

sockfd:

socket 文件描述符

backlog:

排队建立3次握手和刚刚建立3次握手队列的链接数

返回值:

成功返回0 ,失败返回-1 设置errno

accept 函数

#include <sys/type,h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:

socket 文件描述符

addr:

传出参数,返回链接客户端地址信息,含IP地址,和端口号

addrlen:

传入传出参数(值-结果).传入sizeof(addr)大小, 函数返回时返回真正接受到地址结构体的大小 --客户端addr 实际大小

返回值:

成功返回一个新的socket文件描述符,用于和客户端通信, 失败返回-1, 设置errno

connect 函数

#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:

socket 文件描述符

addr:

传出参数,指定服务器端地址信息, 含IP地址和端口号

addrlen:

传入参数,传入sizeof(addr)大小- 服务器地址结构的大小

返回值:

成功返回0. 失败返回-1,设置errno
将原来函数进行封装,在调用时 如果函数错误,直接报错

//wrap.c
#include<stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s){
	perror(s);
	exit(1);
}  

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
			perr_exit("accept error");
	}
	return n;
}

int Bind(int fd, const struct sockaddr *sa, socklen_t salen){
	int n;
	if((n=bind(fd, sa, salen)<0))
		perr_exit("bind error");
	return n;
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen){
	int n;
	if((n=connect(fd,sa,salen))<0){
		perr_exit("connect error");
	}
	return n;
}
int Listen(int fd, int backlog){
	int n;
	if((n=listen(fd,backlog))<0){
		perr_eixt("listen error");
	}
	return n;
}

int Socket(int family, int type, int protocol){
	int n;
	if((n=socket(family, type,protocol))<0)
		perr_exit("socket 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)
		perr_exit("close error");
	return n;
}

ssize_t Readn(int fd, void *vptr, size_t n){
	szie_t nlefd;
	ssize_t nread;
	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)//写入n个字节
{
	size_t nleft;
	ssize_t nwriten;
	const char *ptr;
	ptr=vptr;
	nleft=n;
	while(nleft>0){
		if((nwriten=write(fd,ptr,nleft))<=0){
			if(nwriten<0&&errno==EINTR)
				nwriten=0;
			else
				return -1;
		}
		nleft-=nwriten;
		ptr+=nwriten;
	}
	return n;  //写入n个字节完成
}
static ssize_t my_read(int fd, char *ptr){
	static int read_cnt; //count
	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;// 字符串最后一个元素为0;
			return n-1; //读了多少字节
		}
	}
	*ptr=0;
	return n;
}


wrap.h

#ifndef _WRAP_H_
#define _WRAP_H_void 
perr_exit(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);
static ssize_t my_read(int fd, char *ptr);

#endif

二、epoll高并发服务器

1.epoll底层逻辑:

epoll 来概括图
#include<sys/epoll.h>

函数名功能
epoll_create(int size)创建epoll 监听树
epoll_ctl(int epfd,int op, int fd. struct epoll_event *event)控制某个epoll监控的文件描述符上的事件: 注册,修改,删除
epoll_wait(int epfd, struct peoll_event *events, int maxevents, int timeout)等待所监控文件描述符上有事件的产生

2. epoll 基础API

2.1 epoll_create(int size)

suze: 监听数目
监听文件描述符的个数,与内存大小有关

2.2 epoll_ctl(int epfd,int op, int fd. struct epoll_event *event)

epfd:

为epoll_creat的句柄 --具体是那一课epoll树

op:

表示动作,用3个宏来表示:

EPOLL_CTL_ADD (注册新的fd到epfd)
EPOLL_CTL_MOD (修改已经注册的fd的监听事件)
EPOLL_CTL_DEL (从epfd删除一个fd)

evnet:

告诉内核需要监听的事件

struct epoll_event{
	_uint32_t events; /*Epoll events*/
	epoll_data_t data; /*User data variable*/
};
typedef union epoll_data{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t;

EPOLLIN: 表示对应的文件描述符可以读(包括对端socket 正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLERR: 表示对应的文件描述符发生错误

2.3 epoll_wait(int epfd, struct peoll_event *events, int maxevents, int timeout)

events:

用来存内核得到事件的集合

maxevents:

告知内核这个events有多大, 这个maxevents的值不能大于创建epoll_create() 时的size,

timeout:

超时时间
-1: 阻塞
0: 立刻返回,非阻塞
0: 指定毫秒

返回值:

成功返回有多少文件描述符就绪, 事件到时返回0. 出错时返回-1;

完整代码:

#include <stdio.h>
#include <stdlib,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;
	int 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(seraddr));
	servaddr.sin_family=AF_INET;  //ipv4
	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("epool_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*)&cliaddrm &clilen);
				printf("received from %s at PORT %d \n",
						inet_ntop(AF_INE,&cliaddr.sin_addr,str,sizeof(str)),
						ntols(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(fd);
	return 0;
}

二、其他函數

*ssize_t read(int fd, void buf, size_t count);

参数:

fd:文件描述符
buf: 读取数据的缓冲区
count: 缓冲区大小

返回值:

返回值>0; 实际读到的字节数 buf=1024

两种情况:
返回值==buf;
返回值<buf;

返回值==0;

数据读完(读到文件, 管道,socket 末尾—对端关闭)

返回值==-1; 异常

1. errno==EINTR 被信号中断, 重启/quit
2. errno==EAGAIN;(EWOULLDBLOCK) 非阻塞方式读取, 并没有数据
3. errno==ECONNRESET; 说明连接被重置,需要close(),移除监听队列
4. 其他值,出现错误. ------perror_exit

*ssize_t write(int fd, const void buf, size_t count);

参数:

fd:文件描述符
buf: 待写数据的缓冲区
count: 缓冲区大小

返回值:

成功: 写入字节数
失败: -1, 设置

返回值==buf;
返回值<buf;

相比read和write一次只能下发一个IO请求,并将数据读写到一个指定的缓冲区,readv和writev可以一次性指定多个缓冲区进行读写。

writev与readv

#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);

其中 const struct iovec *iov 設置多個緩衝區

struct iovec{
	void *iov_base; /*starting address of buffer*/
	size_t iov_len; //size of buffer;
}

writev和readv 都是以 iov[0],iov[1],…,iov[n-1] 的順序输出或者读入的, 返回值是输出|读入的字节总数长度之和.
writev 与 readv 详情

总结

学习路径: 《c++程序与设计》,《数据结构》,《操作系统》,《计算机网络》,linux 基础操作, 系统编程, 网络编程;

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值