1.1 协议
概念:协议实现约定好,大家共同遵守的一组规则,协议可以理解为数据传输和数据解释的规则。
1.2 分层模型
OSI 7层模型:物数网传会表应
TCP四层模型:
1.3数据通信过程
发送端层层打包,接收方层层解包
1.4网络应用程序的设计模式
C/S设计模式的优缺点:
优点:可以安装在本地,可以缓存数据,协议的选择灵活
缺点:客户端工具需要由程序员开发,开发周期长工作量大;需要本地安装,对客户的电脑安全有一定影响。
B/S模式:
优点:浏览器不用开发,开发周期短,工作量小
缺点,只能选择http协议,协议选择受限制,不能缓存数据,效率受影响。
1.5 以太网帧格式
以太网帧格式就是包装在网络接口层(数据链路层)的协议
ARP协议:
IP协议:在网络层
协议版本: ipv4, ipv6
16位总长度: 最大65536
8位生存时间ttl(网络连接下一跳的次数): 为了防止网络阻塞
32位源ip地址, 共个4字节!我们熟悉的ip都是点分十进制的,4字节, 每字节对应一个点分位,最大为255 ,实际上就是整形数!
32位目的ip地址
8位协议: 用来区分上层协议是TCP, UDP, ICMP还是IGMP协议.
16位首部校验和: 只校验IP首部, 数据的校验由更高层协议负责.
UDP数据报格式:面向无连接的、不安全的、不可靠的数据报传输。
通过IP地址来确定网络环境中的唯一的一台主机;
主机上使用端口号来区分不同的应用程序.
IP+端口唯一确定唯一一台主机上的一个应用程序.
TCP数据流格式:面向连接的、安全的、可靠的数据流传输协议
序号: TCP是安全可靠的, 每个数据包都带有序号, 当数据包丢失的时候, 需要重传, 要使用序号进行重传. 控制数据有序, 丢包重传.
确认序号: 使用确认序号可以知道对方是否已经收到了, 通过确认序号可以知道哪个序号的数据需要重传.
16位窗口大小–滑动窗口(主要进行流量控制)
传统的进程间通信借助内核提供的IPC机制进行,但是只能限于本机通信。若要跨机通信,就必须使用网络通信,这就需要用到内核提供给用户的socket API函数库。
2.1 网络字节序
大端字节序:也叫高端字节序(网络字节序),是高端地址存放低位数据,低端地址存放高位数据
小端字节序:也叫低端字节序,是低地址存放低位数据,高地址存放高位数据。
大小端进行转换的函数:
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h表示主机host,n表示网络nework,s表示short,l表示long
IP地址转化函数:
p->表示点分十进制的字符串形式
to->到
n->表示network网络
int inet_pton(int af,const char *src,woid *dst);
函数说明:将字符串形式的点分十进制IP转换为大端模式的网络IP
参数说明:
af:AF_INET
src:字符串形式的点分十进制的IP地址
dst:存放转换后的变量的地址
例如:int_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr);
手工也可以计算:
如192.168.232.145, 先将4个正数分别转换为16进制数,
192--->0xC0 168--->0xA8 232--->0xE8 145--->0x91
最后按照大端字节序存放: 0x91E8A8C0, 这个就是4字节的整形值.
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);
函数说明:网络IP转换为字符串形式的点分十进制IP
af:AF_INET
src:网络的整形的IP地址
dst:转换后的IP地址,一般为字符串数组
size:dst的长度
返回值:成功--返回指向dst的指针
失败--返回null,并设置errno
例如: IP地址为010aa8c0, 转换为点分十进制的格式:
01---->1 0a---->10 a8---->168 c0---->192
由于从网络中的IP地址是高端模式, 所以转换为点分十进制后应该为: 192.168.10.1
2.2 SOCKET编程主要的API函数
int socket(int domain,int type,int protocol);
函数描述:创建socket
参数说明:
domain:协议版本
AF_INET IPV4
AF_INET6 IPV6
AF_UNIX AF_LOCAL 本低套接字使用
type:协议类型
SOCK_STREAM 流式,默认使用的协议是TCP协议
SOCK_DGRAM 报式,默认使用的是UDP协议
protocol:
一般填0,表示使用对应类型的默认协议
返回值:成功:返回一个大于0的文件描述符
失败返回-1,并设置errno
当调用socket函数以后,返回一个文件描述符,内核会提供与该文件描述符相对于的读和写缓冲区,同时还有两个队列,分别是请求队列和已连接队列。
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
函数描述:将socket文件描述符和IP,PORT绑定
参数说明:
socket:调用socket函数返回的文件描述符
addr:本地服务器的IP地址和PORT
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY); INADDR_ANY表示使用本机任意有效的可以IP
addrlen:addr变量的占用的内存大小
返回值:成功:返回0;
失败:返回-1,并设置errno
int listen(int sockfd,int backlog);
函数描述:将套接字由主动态变为被动态
参数说明:
sockfd:调用socket函数返回的文件描述符
backlog:同时请求连接的最大个数
返回值: 成功:返回0;
失败:返回-1,并设置errno
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
函数说明:获得一个连接,若当前没有连接则会阻塞等待
函数参数:
sockfd:调用socket函数返回的文件描述符
addr:传出参数,保存客户端的地址信息
addrlen:传入传出参数,addr变量所占内存空间大小
返回值:
成功:返回一个新的文件描述符,用于与客户端通信
失败:返回-1,并设置errno值
accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
从已连接队列中获取一个新的连接, 并获得一个新的文件描述符, 该文件描述符用于和客户端通信. (内核会负责将请求队列中的连接拿到已连接队列中)
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
函数说明:连接服务器
函数参数:
sockfd:调用socket函数返回的文件描述符
addr:服务端的地址信息
addrlen:addr变量的内存大小
返回值:
成功:返回0
失败:返回-1,并设置errno值
接下来就可以使用write和read函数进行读写操作了
读取数据和发送数据:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
对应recv和send这两个函数flags直接填0就可以了.
2.3 使用socket的API函数编写服务端和客户端程序步骤:
服务端:
int main(){
//创建socket用于和服务端进行通信
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd<0){
perror("socket error");
return -1;
}
struct sockaddr_in serv;
//初始化将前n个字节清零
bzero(&serv,sizeof(serv));
serv.sin_family =AF_INET;
serv.sin_port = htons(8888);
//标识适用本机任意可用IP
serv.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
if(ret<0){
perror("bind error");
return -1;
}
//最大是128
listen(lfd,128);
struct sockaddr_in client;
socklen_t len = sizeof(client);
int cfd = accept(lfd ,(struct sockaddr *)&client,&len);
//获取client端的IP和port端口
char sIP[16];
memset(sIP,0x00,sizeof(sIP));
printf("client-->IP:[%s],PORT:[%d]\n",inet_ntop(AF_INET,&client.sin_addr.s_addr,sIP,sizeof(sIP)),ntohs(client.sin_port));
printf("lfd==[%d], cfd==[%d] n", lfd,cfd);
int n = 0;
char buf[1024];
int i = 0;
while(1){
//读数据
memset(buf,0x00,sizeof(buf));
n = read(cfd,buf,sizeof(buf))
if(n<=0){
printf("read error or client close,n==[%d]\n",n);
break;
}
printf("n==[%d] , buf==[%s] n",n, buf);
for(i=0;i<=n;i++){
buf[i] = toupper(buf[i]);
}
// 发送数据
write(cfd,buf,n);
}
//关闭监听文件描述符和通信文件描述符
close(lfd);
close(cfd);
return 0;
}
客户端:
int main(){
//创建socket用于和服务端进行通信
int cfd = socket(AF_INET,SOCK_STREAM,0);
if(cfd<0){
perror("socket error");
return -1;
}
//连接服务器
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr);
printf("[%p]\n",serv.sin_addr.s_addr);
int ret = connect(cfd,(struct sockaddr *)&serv,sizeof(serv));
if(ret<0){
perror("connect error");
return -1;
}
int n = 0;
char buf[256];
while(1){
//读标准输入数据
memset(buf,0x00,sizeof(buf));
n = read(STDIN_FILENO,buf,sizeof(buf));
//发送数据
write(cfd,buf,n);
//读服务端发来的数据
memset(buf,0x00,sizeof(buf));
n = read(cfd,buf,sizeof(buf));
if(n<=0){
printf("read error or server cosed , n==[%d] n",n);
break;
}
printf("n==[%d], buf==[%s]\n",n, buf);
}
close(cfd);
return 0;
}
得到两个文件描述符lfd和cfd,lfd负责监听连接,不参与收发数据;cfd负责和客户端进行通信,有多少个客户端建立了连接,就有多少个cfd,客户端也有一个cfd负责和服务端通信
2.4 三次握手和四次挥手
在客户端与服务端建立连接的时候要经过三次握手的过程,客户端与服务端断开连接的时候要经历四次挥手
SYN:表示请求,ACK:表示确认,服务端发送的SYN和客户端发送的SYN本身也会占1位
三次握手:
通信的时候不再需要SYN标识位了,只有在请求连接的时候需要SYN标识位。
ACK确认包表示给对方发送数据的一个确认,表示你发送的数据都收到了,同时告诉对方下次发送该序号开始的数据,由于每次发送数据都会受到对方发来的确认包,所以可以确认对方是否收到,若没有收到对方法来的确认包,则会重发。
mss最大报文长度,告诉对方最多一次能收多少,不能超过这个长度;
win表示最大缓存空间
FIN是四次挥手结束时的标志。
2.5 滑动窗口
主要作用是进行流量控制的,如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会导致接收缓冲区满而丢失数据,通过滑动窗口来解决这一问题。
win表示告诉对方握着比缓冲区大小是多少,mss表示告诉对方我这边最多一次可以接收多少数据。
客户端给服务端发送包的时候,不一定是非要等到对方返回响应包,由于客户端知道服务端的窗口大小,所以可以持续多次发送,当发送数据达到对方窗口大小了就不再发送,需要等对方处理之后,才可以继续发送。
MTU: 最大传输单元
MTU:通信术语最大传输单元(Maximum Transmission Unit,MTU)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为 单位). 最大传输单元这个参数通常与通信接口有关(网络接口卡、串 口等), 这个值如果设置为太大会导致丢包重传的时候重传的数据量较大, 图中的最大值是1500, 其实是一个经验值.
2.6 粘包
多次数据发送,首尾相连,接收端接收的时候不能正确区分第一次发送多少,第二次发送多少。
粘包问题解决方法:
方案1:包头+数据;有四位的数据长度+数据:00100123456789
其中0010表示数据长度,0123456789表示10个字节长度的数据;另外发送端和接收端可以协商更为复杂的报文结构,相当于双方约定的一个协议
方案2:添加结尾标记:如:\n、$等
方案3:数据包定长:如发送方和接收方约定,每次只发送128个字节的内容,接收方接收定长128个字节就可以了
2.7高并发服务器:
如何支持多个客户端–支持多并发的服务器:由于accept和read函数都会阻塞,当read的时候,不能调用accept接受新的连接,当accept阻塞等待的时候不能read读函数
方案1:使用多进程,可以让父进程接受新连接,让子进程处理与客户端通信;父进程accept接受新连接,然后fork子进程,让子进程处理通信,子进程处理完成后退出,父进程使用SIGCHLD信号回收子进程
处理流程:
多进程版本的网络服务器:父进程接受新的连接,子进程进行通信
#include
#include
#include
#include
#include
#include
#include
#include
#include "wrap.h"
int main()
//创建 socket
int lfd = Socket(AF_INET,SOCK_STREAM,0);
//绑定
struct sockaddr_in serv;
bzero(&serv,sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示本机任意可用IP
//&serv本地服务器的IP和port
Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
//设置监听
Listen(lfd,128);
pid_t pid;
int cfd;
char sIP[16];//保存IP
socklen_t len;
struct sockaddr_in client;
while(1){
//接受新的连接
len = sizeof(client);
memset(sIP,0x00,sizeof(sIP));
//client保存客户端的地址信息,len是传出参数client所占的内存大小,返回cfd用于与客户端进行通信
cfd = Accept(lfd,(struct sockaddr *)&client, &len);
printf("client:[%s) [%dJ\n", inet_ntop(AF_INET, &client.sin_addr.s.addr, sIp, sizeofsIP)), ntohs(client.sin_port));
//接受一个新的连接,创建一个子进程,让子进程完成数据的收发操作
pid = fork();
if(pid<0){
perror("fork error");
exit(-1);
}else if(pid>0){
//关闭通信文件描述符 cfd
close(cfd);
}else if(pid==0){
//关闭监听文件描述符
close(lfd);
int i=0;
int n;
char buf[1024];
while(1){
//读数据
n = Read(cfd,buf,sizeof(buf));
if(n<=0)
printf("read error or client closed, n==[%d]\n",n);
break;
}
printf("[%d]---->:n==[%d], buf==[%s]\n",ntohs(client.sin_port),n, buf);
//将小写转换为大写
for(i=0; i<n; i++){
buf[i] = toupper(buf[i]);
}
//发送数据
Write(cfd,buf,n);
}
//关闭 cfd
close(cfd);
exit(0);
}
}
//关闭监听文件描述符
close(lfd);
return 0;
}
多线程版本的网络服务器:主线程接受新连接,子线程进行通信
#include
#include
#include
#include
#include
#include
#include
#include
#include "wrap.h"
#include
void *mythread(void *arg){
int cfd=*(int *)arg;
char buf[1024];
int n;
int i;
while(1){
memset(buf,0x00,sizeof( buf));
n = Read(cfd,buf,sizeof(buf));
if(n<=0){
printf("read error or client closed ,n==[%d] n",n);
break;
}
printf("n==[%d] , buf==[%s] n",n,buf);
for(i=0;i<n;i++){
buf[i]=toupper(buf[i]);
}
write(cfd,buf,n);
}
close(cfd);
pthread_exit(NULL);
}
int main(){
int lfd = Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv;
bzero(&serv,sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
Listen(lfd,128);
struct sockaddr client;
socklen_t len;
int cfd;
pthread_t pthreadIP;
while(1){
cfd = Accept(lfd,(struct sockaddr *)&client,&len);
//创建子线程
pthread_create(&pthreadIP,NULL,mythread,&cfd);
//设置线程分离
pthread_detach(pthreadIP)
}
close(lfd);
return 0;
}
子线程能否关闭lfd?子线程不能关闭监听文件描述符lfd,子线程和主线程共享文件描述符,而不是复制的
主线程能否关闭cfd?主线程不能关闭cfd,主线程和子线程共享一个cfd,而不是复制的,close之后cfd才被关闭
多个子线程共享cfd,会有什么问题发生?只有最后一个连接能够与服务端进行通信,因为前面的cfd都被覆盖掉。
2.8 2MSL
2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态,当TCP的一端发起主动关闭,在发出最后一个ACK包后,即第3次挥手完成后发送了第四次挥手的ACK包后就进入了TIME_WAIT状态,必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。
为什么需要2MSL?
2.9 端口复用
解决端口复用的问题: bind error: Address already in use, 发生这种情况是在服务端主动关闭连接以后, 接着立刻启动就会报这种错误.
setsockopt函数:
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen);
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
setsockopt(lfd,SOL_SOCKET,SO_REUSEport,&opt,sizeof(int));
在bind之前,socket之后调用
2.10 半关闭状态:如果一方close,另一方没有close,则认为是半关闭状态,处于半关闭状态的时候,可以接收数据,但不能发送数据,相当于把文件描述符的写缓冲区操作关闭了
长连接和短链接的概念:
长连接:连接建立之后一直不关闭的
短链接:连接收发数据完毕之后就关闭
shutdown和close的区别:
shutdown能够把文件描述符上的读或者写操作关闭, 而close关闭文件描述符只是将连接的引用计数的值减1, 当减到0就真正关闭文件描述符了.
如: 调用dup函数或者dup2函数可以复制一个文件描述符, close其中一个并不影响另一个文件描述符, 而shutdown就不同了, 一旦shutdown了其中一个文件描述符, 对所有的文件描述符都有影响 .
2.11 心跳包
用于检查与对方的网络连接是否正常.一般心跳包用于长连接
通信双方需要协商规则(协议),如4个字节长度+数据部分
发送心跳过程:服务A个B发送心跳数据AAAA,服务B收到AAAA之后,给A回复BBBB,此时A收到BBBB之后,认为连接正常,假如A连续发送了多次之后,仍然没有收到B的回复,则认为是连接异常,异常之后A应该重新建立连接.
如何让心跳数据和正常的业务数据不混淆?双方协商协议:例如:4个字节长度+具体数据,先收4个字节的报头数据,如何计算长度,若长度为4,且数据为AAAA,则认为是心跳数据,B会返回应答数据:0004BBBB.
3.1 多路IO计数select,同时监听多个文件描述符,将监控的操作交给内核去处理.
数据类型fd_set:文件描述符集合,本质是位图
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
函数介绍:委托内核监控该文件描述符对应的读,写或者错误事件的发生
参数说明:
nfds:最大的文件描述符+1
readfds:读集合,是一个传入传出参数
传入:指的是告诉内核那些文件描述符需要监控
传出:指的是内核告诉应用程序那些文件描述符发生了变化
writefds:写文件描述符集合(传入传出参数)
execptfds:异常文件描述符集合(传入传出参数)
timeout:
NULL表示永久阻塞,直到有事件发生
0 表示不阻塞,立刻返回,不管是否有监控的事件发生
>0 到指定事件或者有事发生了就返回
返回值:成功返回发生变化的文件描述符的个数
失败返回-1,并设置errno值
宏:
void FD_CLR(int fd,fd_set *set);
功能:将fd从set集合中清除
int FD_ISSET(int fd,fd_set *set);
功能:判断fd是否在集合中
返回值:如果fd在set集合中,返回1,否则返回0
void FD_SET(int fd,fd_set *set);
功能:将fd设置到set集合中
void FD_ZERO(fd_set *set);
功能:初始化set集合
多客户端连接(在不使用多线程和多进程的情况下)
#include
#include
#include
#include
#include
#include
#include
#include
#include "wrap.h"
int main(){
//创建socket
int lfd = Socket(AF_INET,SOCK_STREAM,0);
//设置端口复用
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
//绑定
struct sockaddr_in serv;
bzero(&serv,sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
//监听
Listen(lfd,128);
//定义fd_set类型的变量
fd_set readfds;
fd_set tmpfds;//readfds是传入传出参数,所以需要临时变量来存储
//清空readfds和tmpfds集合
FD_ZERO(&readfds);
FD_ZER0(&tmpfds);
//将lfd加入到readfds中,委托内核监控
FD_SET(lfd,&readfds);
int maxfd = fd;
int nready;
int cfd;
int i=0;
int sockfd;
char buf[1024];
int n=0;
while(1){
tmpfds = readfds;
//tmpfds是输入输出参数
//输入:告诉内核要监测那些文件描述符
//输出:内核告诉应用程序有哪些文件描述符发生了变化
nready = select(maxfd+1,&tmpfds,NULL,NULL,NULL);//select返回发生变化的文件描述符个数
if(nready<0){
//被信号中断
if(errno == EINTR){
continue;
}
break;
}
//有客户端连接请求到来
if(FD_ISSET(lfd,&tmpfds)){
//接受新的客户端连接请求
cfd = Accept(lfd,NULL,NULL);
//将cfd加入到readfds集合中
FD_SET(cfd,&readfds);
//修改内核的监控范围
if(maxfd<cfd){
maxfd=cfd;
}
if(--nready==0){
continue;
}
}
//有数据发来的情况
for(i=lfd+1;i<=maxfd;i++){
sockfd = i;
//判断sockfds文件描述符是否发生变化
if(FD_ISSET(sockfd,&tmpfds)){
//读数据
memset(buf ,0x00,sizeof(buf));
n=Read(sockfd,buf,sizeof(buf));
if(n<=0){
//关闭连接
close(sockfd);
//将sockfd从readfds中删除
FD_CLR(sockfd,&readfds);
}else{
printf("n==[%d], buf==[%s] n",n,buf);
int k= 0;
for(k = 0;k<n;k++){
buf[k]=toupper(buf[k]);
}
Write(sockfd,buf,n);
}
if(--nready==0){
break;
}
}
}
}
close(lfd);
return 0;
}
3.2 多路IO-poll
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
函数说明:跟select类似,监控多路IO,但poll不能跨平台
参数说明:
fds:传入传出参数,实际上是一个结构体数组
fds.fd:要监控的文件描述符
fds.events:
POLLIN:读事件
POLLOUT:写事件
fds.revents:返回的事件
nfds:数组实际有效内容的个数
timeout:超时时间,单位是毫秒
-1:永久阻塞,知道监控的事件发生
0:不管是否有事件发生,立刻返回
>0:知道监控的事件发生或者超时
返回值:
成功:返回就绪事件的个数
失败:返回-1,若timeout=0,poll函数不阻塞,且没有事件发生,此时返回-1,并且errno=EAGAIN,这种情况不应视为错误.
struct pollfd{
int fd; //监控的文件描述符
short events; //要监控的事件 -- 不会被修改
short revents; //返回发生变化的事件 -- 由内核返回
};
当poll函数返回的时候, 结构体当中的fd和events没有发生变化, 究竟有没有事件发生由revents来判断, 所以poll是请求和返回分离.
struct pollfd结构体中的fd成员若赋值为-1, 则poll不会监控.
int main(){
//创建socket
int lfd = Socket(AF_INET,SOCK_STREAM,0);
//允许端口复用
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
//绑定bind
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
//监听listen
Listen(lfd,128);
struct pollfd client[1024];
//监听文件描述符委托给内核监控 监控读事件
client[0].fd=lfd;
client[0].events=POLLIN;
int i=l;
int nready=0;
int maxi=0; //内核监控范围
for(i=1;i<1024;i++){
client[i].fd=-1;
}
int k=0;
int n=0;
int cfd=0;
int sockfd=0;
char buf[1024];
while(1){
nready = poll(client,maxi+1,-1);
if(nready<0){
if(errno==EINTR){
continue;
}
break;
}
//有客户端连接请求
if(client[0].revents==POLLIN){
cfd = Accept(lfd,NULL,NULL);
//寻找client数组中的可用位置
for(i=1;i<1024;i++){
if(client[i].fd==-1){
client[i].fd = cfd;
client[i].events = POLLIN;
break;
}
}
//没有可用位置,则关闭连接
if(i==1024){
Close(cfd);
continue;
}
if(maxi<i){
maxi = i;
}
if(--nready==0){
continue;
}
}
//有数据到来的情况
for(i=1;i<=maxi;i++){
sockfd = client[i].fd;
memset(buf,0x00,sizeof(buf));
//若fd为-1,表示连接已经关闭或者没有连接
if(client[i].fd == -1){
continue;
}
if(client[i].revents==POLLIN){
n=Read(sockfd,buf,sizeof(buf));
if(n<=0){
close(sockfd);
client[i].fd = -1; //fd=-1,表示不再让内核监控
}else{
printf("n==[%d],buf==[%s] n",n, buf);
write(sockfd,buf,n);
}
if(--nready==0){
break;
}
}
}
}
close(lfd);
return 0;
}
3.3 多路IO-epoll
将检测文件描述符的变化委托给内核去处理,然后内核发生变化的文件描述符对应的事件返回给应用程序.
int epoll_create(int size);
函数说明:创建一个树根
参数:
size:最大节点数
返回值:
成功:返回一个大于0的文件描述符,代表整个树的树根
失败:返回-1,并设置errno值
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
函数说明:将要监听的节点在epoll树上添加,删除和修改
参数:
epfd:epoll树根
op:
EPOLL_CTL_ADD:添加事件节点到树上
EOPLL_CTL_DEL:从树上删除事件节点
EOPLL_CTL_MOD:修改树上对应的事件节点
fd:事件节点对应的文件描述符
event:要操作的事件节点
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
event.events常用的有:
EPOLLIN: 读事件
EPOLLOUT: 写事件
EPOLLERR: 错误事件
EPOLLET: 边缘触发模式
event.fd: 要监控的事件对应的文件描述符
int epoll_wait(int pefd,struct epoll_event *events,int maxevents,int timeout);
函数说明:等待内核返回事件发生
参数说明:
epfd:epoll树根
events:传出参数,时间结构体数组
maxevents:数组大小
timeout:
-1:表示永久阻塞
0:立即返回
>0:表示超时等待事件
返回值:
成功:返回发生事件的个数
失败:若timeout=0,没有事件发生,则返回;返回-1,设置errno值
epoll_wait的events是一个传出参数, 调用epoll_ctl传递给内核什么值, 当epoll_wait返回的时候, 内核就传回什么值,不会对struct event的结构体变量的值做任何修改.
测试代码:
int main(){
int lfd = Socket(AF_INET,SOCK_STREAM,0);
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
struct sockaddr_in serv;
bzero(&serv,sizeof(serv));
serv.sin_family=AF_INET;
serv.sin_port=htons(8888);
serv.sin_addr.s_addr=htonl(INADDR_ANY);
Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
Listen(lfd,128);
//创建epoll树
int epfd = epoll_create(1024);
//将lfd上树
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd=lfd;
//要监听的节点添加到epoll树上
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
struct epoll_event events[1024]; //定义一个传出数组
int nready=0;
int i=0;
int n=0;
char buf[1024];
int cfd=0;
int sockfd=0;
while(1){
//阻塞,内核等待事件发生,保存修改或者发生事件的文件描述符
nready = epoll_wait(epfd,events,1024,-1);
if(nready<0){
if(errno==EINTR){
continue;
}
break;
}
//有事件发生时,有客户端连接请求
for(i=0;i<nready;i++){
sockfd = events[i].data.fd;
//判断是否是lfd,是则accept
if(sockfd==lfd){
cfd=Accept(lfd,NULL,NULL);
ev.data.fd=cfd;
ev.events = EPOLLIN;
//cfd上树
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
continue;
}
//有客户端发送数据过来
memset(buf,0x00,sizeof(buf));
n = Read(sockfd,buf,sizeof(buf));
if(n<=0){
//读不到数据,关闭通信文件描述符,从树上删除
close(sockfd);
epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);
perror("read error or client close");
continue;
}
printf("n==[%d], buf==[%s] n",n,buf);
Write(sockfd,buf,n);
}
}
close(epfd);
close(lfd);
return 0;
}
3.4 epoll的两种工作模式
epoll的两种模式ET和LT模式
水平触发LT:高电平代表1,只要缓冲区中有数据,就一直通知,epoll默认情况下是LT模式,在这种模式下若读数据一次性没读完,缓冲区中还有可读数据,则epoll_wait还会再通知.
边缘触发ET:电平有变化就代表1,缓冲区中有数据指挥通知一次,之后再有数据才会通知(若是读数据的时候没有读完,则剩余的数据不会再通知,直到有新的数据到来)epoll设置为ET模式,若读数据的时候一次性没有读完,则epoll_wait不再通知,知道下次有新的数据发来.
在ET模式下,如何在epoll_wait返回一次的情况下读完数据:循环读数据,直到读完数据,但是读完数据之后会阻塞
若一次性读完还需设置什么?将通信文件描述符设置为非阻塞(fcntl函数)
3.5 epoll反应堆
反应堆:一个小事件触发一系列反应
epoll反应堆思想:c++封装思想(把数据和操作封装到一起),将描述符,事件,对应的处理方法封装在一起,当描述符对应的事件发生了,自动调用处理方法.
epoll反应堆的核心思想是:在调用epoll_ctl函数的时候,将events上树的时候,利用epoll_data_t的ptr成员,将一个文件描述符,事件和回调函数封装成一个结构体,然后让ptr指向这个结构体,然后调用epoll_wait函数返回的时候,可用得到具体的events,然后获得events结构体中的events.data.ptr指针,ptr指针指向的结构体中有回调函数,最终可以调用这个回调函数.
线程池:若干个线程组合到一起,形成线程池.
线程池和任务池:任务池相当于共享资源,所以需要使用互斥锁,当任务池中没有任务的时候需要让线程池阻塞,所以需要使用条件变量.
线程池流程:
#include
#include
#include
#include
#include
#include
#include
#include
#include "threadpool.h"
#define DEFAULT_TIME 10 /*10s检测一次*/
#define MIN_WAIT_TASK_NUM 10 /*如果queue_size > MIN_WAIT_TASK_NUM 添加新的线程到线程池*/
#define DEFAULT_THREAD_VARY 10 /*每次创建和销毁线程的个数*/
#define true 1
#define false 0
typedef struct
{
void *(*function)(void *); /* 函数指针,回调函数 */
void *arg; /* 上面函数的参数 */
} threadpool_task_t; /* 各子线程任务结构体 */
/* 描述线程池相关信息 */
struct threadpool_t
{
pthread_mutex_t lock; /* 用于锁住本结构体 */
pthread_mutex_t thread_counter; /* 记录忙状态线程个数de琐 -- busy_thr_num */
pthread_cond_t queue_not_full; /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
pthread_cond_t queue_not_empty; /* 任务队列里不为空时,通知等待任务的线程 */
pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */
pthread_t adjust_tid; /* 存管理线程tid */
threadpool_task_t *task_queue; /* 任务队列(数组首地址) */
int min_thr_num; /* 线程池最小线程数 */
int max_thr_num; /* 线程池最大线程数 */
int live_thr_num; /* 当前存活线程个数 */
int busy_thr_num; /* 忙状态线程个数 */
int wait_exit_thr_num; /* 要销毁的线程个数 */
int queue_front; /* task_queue队头下标 */
int queue_rear; /* task_queue队尾下标 */
int queue_size; /* task_queue队中实际任务数 */
int queue_max_size; /* task_queue队列可容纳任务数上限 */
int shutdown; /* 标志位,线程池使用状态,true或false */
};
void *threadpool_thread(void *threadpool);
void *adjust_thread(void *threadpool);
int is_thread_alive(pthread_t tid);
int threadpool_free(threadpool_t *pool);
//threadpool_create(3,100,100);
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
int i;
threadpool_t *pool = NULL;
do
{
if((pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL)
{
printf("malloc threadpool fail");
break; /*跳出do while*/
}
pool->min_thr_num = min_thr_num;
pool->max_thr_num = max_thr_num;
pool->busy_thr_num = 0;
pool->live_thr_num = min_thr_num; /* 活着的线程数 初值=最小线程数 */
pool->wait_exit_thr_num = 0;
pool->queue_size = 0; /* 有0个产品 */
pool->queue_max_size = queue_max_size;
pool->queue_front = 0;
pool->queue_rear = 0;
pool->shutdown = false; /* 不关闭线程池 */
/* 根据最大线程上限数, 给工作线程数组开辟空间, 并清零 */
pool->threads = (pthread_t *)malloc(sizeof(pthread_t)*max_thr_num);
if (pool->threads == NULL)
{
printf("malloc threads fail");
break;
}
memset(pool->threads, 0, sizeof(pthread_t)*max_thr_num);
/* 队列开辟空间 */
pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_max_size);
if (pool->task_queue == NULL)
{
printf("malloc task_queue fail\n");
break;
}
/* 初始化互斥琐、条件变量 */
if (pthread_mutex_init(&(pool->lock), NULL) != 0
|| pthread_mutex_init(&(pool->thread_counter), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_empty), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_full), NULL) != 0)
{
printf("init the lock or cond fail\n");
break;
}
//启动工作线程
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
for (i = 0; i < min_thr_num; i++)
{
pthread_create(&(pool->threads[i]), &attr, threadpool_thread, (void *)pool);/*pool指向当前线程池*/
printf("start thread 0x%x...\n", (unsigned int)pool->threads[i]);
}
//创建管理者线程
pthread_create(&(pool->adjust_tid), &attr, adjust_thread, (void *)pool);
return pool;
} while (0);
/* 前面代码调用失败时,释放poll存储空间 */
threadpool_free(pool);
return NULL;
}
/* 向线程池中 添加一个任务 */
//threadpool_add(thp, process, (void*)&num[i]); /* 向线程池中添加任务 process: 小写---->大写*/
int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg)
{
pthread_mutex_lock(&(pool->lock));
/* ==为真,队列已经满, 调wait阻塞 */
while ((pool->queue_size == pool->queue_max_size) && (!pool->shutdown))
{
pthread_cond_wait(&(pool->queue_not_full), &(pool->lock));
}
if (pool->shutdown)
{
pthread_cond_broadcast(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}
/* 清空 工作线程 调用的回调函数 的参数arg */
if (pool->task_queue[pool->queue_rear].arg != NULL)
{
pool->task_queue[pool->queue_rear].arg = NULL;
}
/*添加任务到任务队列里*/
pool->task_queue[pool->queue_rear].function = function;
pool->task_queue[pool->queue_rear].arg = arg;
pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size; /* 队尾指针移动, 模拟环形 */
pool->queue_size++;
/*添加完任务后,队列不为空,唤醒线程池中 等待处理任务的线程*/
pthread_cond_signal(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}
/* 线程池中各个工作线程 */
void *threadpool_thread(void *threadpool)
{
threadpool_t *pool = (threadpool_t *)threadpool;
threadpool_task_t task;
while (true)
{
/* Lock must be taken to wait on conditional variable */
/*刚创建出线程,等待任务队列里有任务,否则阻塞等待任务队列里有任务后再唤醒接收任务*/
pthread_mutex_lock(&(pool->lock));
/*queue_size == 0 说明没有任务,调 wait 阻塞在条件变量上, 若有任务,跳过该while*/
while ((pool->queue_size == 0) && (!pool->shutdown))
{
printf("thread 0x%x is waiting\n", (unsigned int)pthread_self());
pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));//暂停到这
/*清除指定数目的空闲线程,如果要结束的线程个数大于0,结束线程*/
if (pool->wait_exit_thr_num > 0)
{
pool->wait_exit_thr_num--;
/*如果线程池里线程个数大于最小值时可以结束当前线程*/
if (pool->live_thr_num > pool->min_thr_num)
{
printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
pool->live_thr_num--;
pthread_mutex_unlock(&(pool->lock));
//pthread_detach(pthread_self());
pthread_exit(NULL);
}
}
}
/*如果指定了true,要关闭线程池里的每个线程,自行退出处理---销毁线程池*/
if (pool->shutdown)
{
pthread_mutex_unlock(&(pool->lock));
printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
//pthread_detach(pthread_self());
pthread_exit(NULL); /* 线程自行结束 */
}
/*从任务队列里获取任务, 是一个出队操作*/
task.function = pool->task_queue[pool->queue_front].function;
task.arg = pool->task_queue[pool->queue_front].arg;
pool->queue_front = (pool->queue_front + 1) % pool->queue_max_size; /* 出队,模拟环形队列 */
pool->queue_size--;
/*通知可以有新的任务添加进来*/
pthread_cond_broadcast(&(pool->queue_not_full));
/*任务取出后,立即将 线程池琐 释放*/
pthread_mutex_unlock(&(pool->lock));
/*执行任务*/
printf("thread 0x%x start working\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter)); /*忙状态线程数变量琐*/
pool->busy_thr_num++; /*忙状态线程数+1*/
pthread_mutex_unlock(&(pool->thread_counter));
(*(task.function))(task.arg); /*执行回调函数任务*/
//task.function(task.arg); /*执行回调函数任务*/
/*任务结束处理*/
printf("thread 0x%x end working\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter));
pool->busy_thr_num--; /*处理掉一个任务,忙状态数线程数-1*/
pthread_mutex_unlock(&(pool->thread_counter));
}
pthread_exit(NULL);
}
/* 管理线程 */
void *adjust_thread(void *threadpool)
{
int i;
threadpool_t *pool = (threadpool_t *)threadpool;
while (!pool->shutdown)
{
sleep(DEFAULT_TIME); /*定时 对线程池管理*/
pthread_mutex_lock(&(pool->lock));
int queue_size = pool->queue_size; /* 关注 任务数 */
int live_thr_num = pool->live_thr_num; /* 存活 线程数 */
pthread_mutex_unlock(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
int busy_thr_num = pool->busy_thr_num; /* 忙着的线程数 */
pthread_mutex_unlock(&(pool->thread_counter));
/* 创建新线程 算法: 任务数大于最小线程池个数, 且存活的线程数少于最大线程个数时 如:30>=10 && 40<100*/
if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num)
{
pthread_mutex_lock(&(pool->lock));
int add = 0;
/*一次增加 DEFAULT_THREAD 个线程*/
for (i = 0; i < pool->max_thr_num && add < DEFAULT_THREAD_VARY
&& pool->live_thr_num < pool->max_thr_num; i++)
{
if (pool->threads[i] == 0 || !is_thread_alive(pool->threads[i]))
{
pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);
add++;
pool->live_thr_num++;
}
}
pthread_mutex_unlock(&(pool->lock));
}
/* 销毁多余的空闲线程 算法:忙线程X2 小于 存活的线程数 且 存活的线程数 大于 最小线程数时*/
if ((busy_thr_num * 2) < live_thr_num && live_thr_num > pool->min_thr_num)
{
/* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */
pthread_mutex_lock(&(pool->lock));
pool->wait_exit_thr_num = DEFAULT_THREAD_VARY; /* 要销毁的线程数 设置为10 */
pthread_mutex_unlock(&(pool->lock));
for (i = 0; i < DEFAULT_THREAD_VARY; i++)
{
/* 通知处在空闲状态的线程, 他们会自行终止*/
pthread_cond_signal(&(pool->queue_not_empty));
}
}
}
return NULL;
}
int threadpool_destroy(threadpool_t *pool)
{
int i;
if (pool == NULL)
{
return -1;
}
pool->shutdown = true;
/*先销毁管理线程*/
//pthread_join(pool->adjust_tid, NULL);
for (i = 0; i < pool->live_thr_num; i++)
{
/*通知所有的空闲线程*/
pthread_cond_broadcast(&(pool->queue_not_empty));
}
/*for (i = 0; i < pool->live_thr_num; i++)
{
pthread_join(pool->threads[i], NULL);
}*/
threadpool_free(pool);
return 0;
}
int threadpool_free(threadpool_t *pool)
{
if (pool == NULL)
{
return -1;
}
if (pool->task_queue)
{
free(pool->task_queue);
}
if (pool->threads)
{
free(pool->threads);
pthread_mutex_lock(&(pool->lock));
pthread_mutex_destroy(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
pthread_mutex_destroy(&(pool->thread_counter));
pthread_cond_destroy(&(pool->queue_not_empty));
pthread_cond_destroy(&(pool->queue_not_full));
}
free(pool);
pool = NULL;
return 0;
}
int threadpool_all_threadnum(threadpool_t *pool)
{
int all_threadnum = -1;
pthread_mutex_lock(&(pool->lock));
all_threadnum = pool->live_thr_num;
pthread_mutex_unlock(&(pool->lock));
return all_threadnum;
}
int threadpool_busy_threadnum(threadpool_t *pool)
{
int busy_threadnum = -1;
pthread_mutex_lock(&(pool->thread_counter));
busy_threadnum = pool->busy_thr_num;
pthread_mutex_unlock(&(pool->thread_counter));
return busy_threadnum;
}
int is_thread_alive(pthread_t tid)
{
int kill_rc = pthread_kill(tid, 0); //发0号信号,测试线程是否存活
if (kill_rc == ESRCH)
{
return false;
}
return true;
}
/*测试*/
#if 1
/* 线程池中的线程,模拟处理业务 */
void *process(void *arg)
{
printf("thread 0x%x working on task %d\n ",(unsigned int)pthread_self(),*(int *)arg);
sleep(1);
printf("task %d is end\n", *(int *)arg);
return NULL;
}
int main(void)
{
/*threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);*/
threadpool_t *thp = threadpool_create(3,100,100); /*创建线程池,池里最小3个线程,最大100,队列最大100*/
printf("pool inited");
//int *num = (int *)malloc(sizeof(int)*20);
int num[20], i;
for (i = 0; i < 20; i++)
{
num[i]=i;
printf("add task %d\n",i);
threadpool_add(thp, process, (void*)&num[i]); /* 向线程池中添加任务 */
}
sleep(10); /* 等子线程完成任务 */
threadpool_destroy(thp);
return 0;
}
#endif
TCP:传输控制协议,面向连接的,稳定的,可靠的,安全的数据流传递
稳定和可靠:丢包重传
数据有序:序号和确认序号(SYN和ACK)
流量控制:滑动窗口
UDP:用户数据报协议,面向无连接的,不稳定,不可靠,不安全的数据报传递,更像是收发短信,UDP传输不需要建立连接,传输效率更高,在稳定的局域网环境内相对可靠
UDP通信相关函数:
size_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen);
函数说明:接收消息
参数说明:
sockfd:套接字(文件描述符)
buf:要接受的缓冲区
len:缓冲区的长度
flags:标志位,一般填0
src_addr:原地址 传出参数
addrlen:发送方地址长度
返回值: 成功返回读到的字节数
失败:返回-1,设置errno
调用该函数相当于TCP通信的recv+accept函数
size_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr *dest_addr,socklen_t addrlen);
函数说明:发送数据
参数说明:
sockfd:套接字
dest_addr:目的地址
addrlen:目的地址长度
返回值:成功返回写入的字节数
失败:返回-1,设置errno
UDP通信服务端流程:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(){
int cfd = socket(AF_INET,SOCK_DGRAM,0);
if(cfd<0){
perror("socket error");
return -1;
}
struct sockaddr in serv;
struct sockaddr in client;
bzero(&serv,sizeof(serv));
serv.sin_family=AF_INET;
serv.sin_port = htons(8888);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
bind(cfd,(struct sockaddr *)&serv,sizeof(serv));
int n;
char buf[1024];
socklen_t len;
int i=0;
while(1){
memset(buf,0x00,sizeof(buf));
len = sizeof(client);
//读取数据
n=recvfrom(cfd,buf,sizeof(buf),0,(struct sockaddr *)&client,&len);
for(i=0;i<n;i++){
buf[i] = toupper(buf[i]);
}
printf("[%d]:n==[%d],buf==[%s]\n",ntohs(client.sin_port),n,buf);
//发送数据
sendto(cfd,buf,n,0,(struct sockaddr *)&client,len);
}
//关闭套接字
close(cfd);
return 0;
}
//可连接多个客户端,当服务端关闭客户端依然建立连接,但是不能发送数据
UDP客户端流程:
int main(){
//创建socket
int cfd = socket(AF_INET,SOCK_DGRAM,0);
if(cfd<0){
perror("socket error");
return -1;
}
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr);
int n;
char buf[1024];
socklen_t len;
int i=0;
while(1){
//读标准输入数据
memset(buf,0x00,sizeof(buf));
n= read(STDIN_FILENO,buf,sizeof(buf));
//发送数据
sendto(cfd,buf,n,0,(struct sockaddr *)&serv,sizeof(serv));
//读取数据
memset(buf,0x00,sizeof(buf));
n=recvfrom(cfd,buf,sizeof(buf),0,NULL,NULL);
printf("n==[%d], buf==[%s] n",n,buf);
}
close(cfd);
return 0;
}
使用nc命令进行测试: nc -u 127.1 8888
本地socket通信:
man 7 unix
int socket(int domain,int type,int protocol);
函数说明:创建本地域socket
函数参数:
domain:AF_UNIX or AF_LOCAL
type:SOCK_STREAM 或者 SOCK_DGRAM
PROTOCOL:0表示使用默认协议
函数返回值: 成功:返回文件描述符
失败:返回-1,并设置errno值
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
函数说明:绑定套接字
函数参数:
socket:由socket函数返回的文件描述符
addr:本地地址
addlen:本地地址长度
函数返回值:成功:返回文件描述符
失败:返回-1,并设置errno值
bind函数会自动创建socket文件,若在调用bind函数之前,socket文件已经存在,则调用bind会报错,可用使用unlink函数在bind之前先删除文件
本地套接字服务器的流程:
可以使用TCP的方式, 必须按照tcp的流程
也可以使用UDP的方式, 必须按照udp的流程
tcp的本地套接字服务器流程:
创建套接字 socket(AF_UNIX,SOCK_STREAM,0)
绑定 struct sockaddr_un &强转
侦听 listen
获得新连接 accept
循环通信 read-write
关闭文件描述符 close
tcp本地套接字客户端流程:
调用socket创建套接字
调用bind函数将socket文件描述和socket文件进行绑定.
不是必须的, 若无显示绑定会进行隐式绑定,但服务器不知道谁连接了.
调用connect函数连接服务端
循环通信read-write
关闭文件描述符 close
服务器端代码:
int main()
{
//创建socket
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(lfd<0)
{
perror("socket error");
return -1;
}
//删除socket文件,避免bind失败
unlink("./server.sock");
//绑定bind
struct sockaddr_un serv;
bzero(&serv, sizeof(serv));
serv.sun_family = AF_UNIX;
strcpy(serv.sun_path, "./server.sock");
int ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret<0)
{
perror("bind error");
return -1;
}
//监听listen
listen(lfd, 10);
//接收新的连接-accept
struct sockaddr_un client;
bzero(&client, sizeof(client));
int len = sizeof(client);
int cfd = accept(lfd, (struct sockaddr *)&client, &len);
if(cfd<0)
{
perror("accept error");
return -1;
}
printf("client->[%s]\n", client.sun_path);
int n;
char buf[1024];
while(1)
{
//读数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n<=0)
{
printf("read error or client close, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
//发送数据
write(cfd, buf, n);
}
close(lfd);
return 0;
}
客户端代码:
int main()
{
//创建socket
int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(cfd<0)
{
perror("socket error");
return -1;
}
//删除socket文件,避免bind失败
unlink("./client.sock");
//绑定bind
struct sockaddr_un client;
bzero(&client, sizeof(client));
client.sun_family = AF_UNIX;
strcpy(client.sun_path, "./client.sock");
int ret = bind(cfd, (struct sockaddr *)&client, sizeof(client));
if(ret<0)
{
perror("bind error");
return -1;
}
struct sockaddr_un serv;
bzero(&serv, sizeof(serv));
serv.sun_family = AF_UNIX;
strcpy(serv.sun_path, "./server.sock");
ret = connect(cfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret<0)
{
perror("connect error");
return -1;
}
int n;
char buf[1024];
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(STDIN_FILENO, buf, sizeof(buf));
//发送数据
write(cfd, buf, n);
//读数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n<=0)
{
printf("read error or client close, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
}
close(cfd);
return 0;
}
TCP和UDP的区别:TCP是建立连接的一对一的服务,类似于打电话,必须对方接听才能进行通话,UDP是无连接的,类似于发短信,无论对方什么状态都可以进行通信.
6.1 libevent的地基event_base:使用libevent函数之前需要分配一个或者多个event_base结构体,每个event_base结构体持有一个事件集合,可以检测以确定哪个事件是激活的,event_base结构相当于epoll红黑树的树根节点,每个event_base都有一种用于检测某种事件已经就绪的方法.
struct event_base *event_base_new(void);
函数说明:获得event_base结构
返回值:成功返回event_base结构
失败返回NULL
void event_base_free(struct event_base *);
函数说明:释放event_base指针
int event_reinit(struct event_base *base);
函数说明:如果有子进程,且子进程也要使用base,则子进程需要对event_base重新初始化,此时需要调用event_reinit函数
函数参数:由event_base_new返回的执行event_base结构的指针
返回值:成功返回0,失败返回-1
const char **event_get_supported_methods(void);
函数说明:获得当前系统支持的方法有哪些
返回值:返回二维数组
const char *event_base_get_method(const struct event_base *base);
函数说明:获得当前base节点使用的多路IO方法
函数参数:event_base结构的base指针
返回值:获得当前base节点使用的多路IO方法的指针
查看当前系统支持的多路IO方法和当前所使用的方法:
#include
#include
#include
#include
int main(){
int i=0;
//获取当前系统支持的方法
const char **p = event_get_supported_methods();
while(p[i]!=NULL){
printf("%s \t",p[i++]);
}
printf("\n");
//获取地基节点
struct event_base*base = event_base_new();
if(base==NULL){
printf("event_base_new errorn");
return -1;
}
//获取当前系统使用的方法
const char *pp = event_base_get_method(base);
printf("%s\n",pp);
//释放节点
event_base_free(base);
}
//ln -s /usr/lib/libevent-2.0.so.5 /usr/lib64/libevent-2.0.so.5
等待事件产生-循环等待event_loop:libevent在地基打好之后,需要等待事件的产生,也就是等待事件被激活,所以程序不能退出,对于epoll来说,我们需要自己控制循环,接口如下:
int event_base_loop(struct event_base *base,int flags);
函数说明:进入循环等待事件
参数说明:
base:由event_base_new函数返回的指向event_base结构的指针
flags的取值:
#define EVLOOP_ONCE 0x01 只触发一次,如果事件没有被触发,阻塞等待
#define EVLOOP_NONBLOCK 0x02 非阻塞方式检测事件是否被触发,不管事件触发与否,都会立即返回
int event_base_dispatch(struct event_base *base);
函数说明:进入循环等待事件
参数说明:由event_base_neew函数返回的指向event_base结构的指针
调用该函数,相当于没有设置标志位的event_base_loop.程序将会一直运行,直到没有需要检测的事情了,或者被结束循环的API终止.
int event_base_loopexit(struct event_base *base,const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);
struct timeval{
long tv_sec;
long tv_usec;
};
两个函数的区别是如果正在执行激活时间的回调函数,那么event_base_loopexit将在事件回调执行结束后终止循环,而event_base_loopbreak会立即终止循环.
使用libevent库的步骤:
typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
struct event *event_new(struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg);
函数说明:event_new负责创建event结构指针,同时指定对应的地基base,还有对应的文件描述符,事件,以及回调函数和回调函数参数
参数说明:
base:对应的根节点 -- 地基
fd:要监听的文件描述符
events:要监听的事件
#define EV_TIMEOUT 0x01 //超时事件
#define EV_READ 0x02 //读事件
#define EV_WRITE 0x04 //写事件
#define EV_SIGNAL 0x08 //信号事件
#define EV_PERSIST 0x10 //周期性触发
#define EV_ET 0x20 //边缘触发,如果底层模型支持设置,则有效,若不支持则无效
若想设置持续的读事件则:EV_READ | EV_PERSIST
cd回调函数:
typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
参数:对应于event_new函数的fd, event和arg
int event_add(struct event *ev,const struct timeval *timeout);
函数说明:将非未决态事件转为未决态,相当于调用epoll_ctl函数(EPOLL_CTL_ADD),开始监听事件是否产生,相当于epoll的上树操作
参数说明:
ev:调用event_new创建的事件
timeout:限时等待事件的产生,也可以设置NULL,没有限时
int event_del(struct event *ev);
函数说明:将事件从未决态变为非未决态,相当于epoll的下树(epoll_ctl(EPOLL_CTL_DEL))操作
参数说明:
ev指的是由event_new创建的事件
void event_free(struct event *ev);
函数说明:释放由event_new申请的event节点
编写基于event实现的tcp服务器
//编写libevent服务端
#include
#include
#include
#include
#include
#include
#include
#include
struct event *connev = NULL;
//typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
void readcb(evutil_socket_t fd, short events, void *arg)
{
int n;
char buf[1024];
memset(buf, 0x00, sizeof(buf));
n = read(fd, buf, sizeof(buf));
if(n<=0)
{
close(fd);
//将通信文件描述符对应的事件从base地基上删除
event_del(connev);
}
else
{
write(fd, buf, n);
}
}
void conncb(evutil_socket_t fd, short events, void *arg)
{
struct event_base *base = (struct event_base *)arg;
//接受新的客户端连接
int cfd = accept(fd, NULL, NULL);
if(cfd>0)
{
//创建通信文件描述符对应的事件并设置回调函数为readcb
connev = event_new(base, cfd, EV_READ|EV_PERSIST, readcb, NULL);
if(connev==NULL)
{
//退出循环
event_base_loopexit(base, NULL);
}
//将通信文件描述符对应的事件上event_base地基
event_add(connev, NULL);
}
}
int main()
{
//创建socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
//设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//绑定
struct sockaddr_in serv;
bzero(&serv, sizeof(serv));
serv.sin_addr.s_addr = htonl(INADDR_ANY);
serv.sin_port = htons(8888);
serv.sin_family = AF_INET;
bind(lfd, (struct sockaddr*)&serv, sizeof(serv));
//监听
listen(lfd, 120);
//创建地基
struct event_base *base = event_base_new();
if(base==NULL)
{
printf("event_base_new error\n");
return -1;
}
//创建监听文件描述符对应的事件
//struct event *event_new(struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg);
struct event *ev = event_new(base, lfd, EV_READ|EV_PERSIST, conncb, base);
if(ev==NULL)
{
printf("event_new error\n");
return -1;
}
//将新的事件节点上base地基
event_add(ev, NULL);
//进入事件循环等待
event_base_dispatch(base);
//释放资源
event_base_free(base);
event_free(ev);
close(lfd);
return 0;
}
//假如有3个客户端连接,将第一个关闭时,后面两个将不能通信,若直接关闭第三个,则前两个还可以通信.是因为,connev是全局变量,任何进程均可访问,event_del(connev)时会将最后一个下树.
//解决方法:通过事件来判断把那个客户端下树,fd和事件在结构体中是一一对应的
6.2 自带buffer事件,bufferevent内部有两个缓冲区,以及一个文件描述符.一个文件描述符有读和写两个缓冲区,bufferevent同样也带有两个缓冲区,还有就是libevent事件驱动的核心回调函数,四个缓冲区以及触发回调的关系如下:
一个bufferevent对应两个缓冲区, 三个回调函数, 分别是写回调, 读回调和事件回调.
bufferevent的三个回调函数:
主要函数:
struct bufferevent *bufferevent_socket_new(struct event_base *base,evutil_socket_t fd,int options);
函数说明:bufferevent_socket_new对已经存在socket创建bufferevent事件,可用于后面的连接监听器的回调函数中.
参数说明:
base:对应根节点
fd:文件描述符
options:bufferevent的选项
BEV_OPT_CLOSE_ON_FREE 释放bufferevent自动关闭底层接口(当bufferevent被释放后,文件描述符也随之被close)
BEV_OPT_THREADSAFE 使bufferevent能够在多线程下是安全的
int bufferevent_socket_connect(struct bufferevent *bev,struct sockaddr *serv,int socklen);
函数说明:该函数封装了底层的socket与connect接口,通过调用此函数可以将bufferevent事件与通信的socket进行绑定
参数说明:
bev:需要提前初始化bufferevent事件
serv:对端的ip地址,端口,协议的结构指针
socklen:描述serv的长度
说明:调用此函数以后,通信的socket与bufferevent缓冲区做了绑定,
后面调用bufferevent_setcb函数以后,会对bufferevent缓冲区的读写操作的事件设置回调函数,
当往缓冲区中写数据的时候会触发写回调函数,
当数据从socket的内核缓冲区读到bufferevent读缓冲区中的时候会触发读回调函数
void bufferevent_free(struct bufferevent *bufev);
函数说明:释放bufferevent
void bufferevent_setcb(struct bufferevent *bufev,
bufferevent_data_cb readcb,bufferevent_data_cb writecb,
buffer_event_cb eventcb,void *cbarg);
函数说明:bufferevent_setcb用于设置bufferevent的回调函数,readcb,writecb,eventcb分别对应了读回调,写回调和事件回调,cbarg代表回调函数的参数.
回调函数的原型:
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short what, void *ctx);
What 代表 对应的事件
BEV_EVENT_EOF--遇到文件结束指示
BEV_EVENT_ERROR--发生错误
BEV_EVENT_TIMEOUT--发生超时
BEV_EVENT_CONNECTED--请求的过程中连接已经完成
int bufferevent_write(struct bufferevent *bufev,const void *data,size_t size);
函数说明:bufferevent_write是将data的数据写到bufferevent的写缓冲区
size_t bufferevent_read(struct bufferevent *bufev,void *data,size_t size);
函数说明:bufferevent_read是将bufferevent的读缓冲区数据读到data中,同时将读到的数据从bufferevent的读缓冲区中清除
int bufferevent_enable(struct bufferevent *bufev,short event);
int bufferevent_disable(struct bufferevent *bufev,short event);
函数说明:bufferevent_enable与bufferevent_disable是设置事件是否生效, 如果设置为disable, 事件回调将不会被触发
6.3 链接监听器evconnlistener
链接监听器封装了底层的socket通信相关函数,比如:socket,bind,listen,accept.链接监听器创建后实际上调用了socket,bind,listen.此时等待新的客户端连接到来,如果有新的客户端连接,那么内部先进性调用accept处理,然后调用用户指定的回调函数.
struct evconnlistener *evconnlistener_new_bind(struct event_base *base,
evconnlistener_cb cb, void *ptr, unsigned flags, int backlog,
const struct sockaddr *sa, int socklen);
函数说明:在没有套接字的情况下对连接监听器进行初始化
参数:
cb:是有新连接之后的回调函数,但是注意这个回调函数触发的时候,链接器已经处理好新连接了,并将与新连接通信的描述符交给回调函数
ptr是回调函数的参数
backlog是listen函数的关键参数,若backlog是-1,那么监听器会自动选择一个合适的值,如果填0,那么监听器会认为listen函数已经被调用过了
sa和socklen是bind函数的参数
flags:
LEV_OPT_LEAVE_SOCKETS_BLOCKING 文件描述符为阻塞的
LEV_OPT_CLOSE_ON_FREE 关闭时自动释放
LEV_OPT_REUSEABLE 端口复用
LEV_OPT_THREADSAFE 分配锁, 线程安全
回调函数:
typedef void (*evconnlistener_cb)(struct evconnlistener *evl, evutil_socket_t fd, struct sockaddr *cliaddr, int socklen, void *ptr);
参数:
fd:与客户端通信的描述符,cliaaddr对应的也是新连接的对端地址信息
void evconnlistener_free(struct evconnlistener *lev);
函数说明: 释放链接监听器
int evconnlistener_enable(struct evconnlistener *lev);
函数说明: 使链接监听器生效
int evconnlistener_disable(struct evconnlistener *lev);
函数说明: 使链接监听器失效
<html> 开始 和</html> 结束,属于html的根标签
<head></head> 头部标签,头部标签内一般有 <title></title>
<body></body> 主体标签,一般用于显示内容
题目标签:
共有6种,<h1>,<h2>,…<h6>,其中<h1>最大,<h6>最小
文本标签:
<font size={1-7} color=red></font>
换行标签:<br/>
水平线:<hr/>
列表标签:
<ul>和<ol>无序和有序
<ul>
<li>列表内容1</li>
<li>列表内容2</li>
…
</ul>
无序列表可以设置type属性:
实心圆圈:type=disc
空心圆圈:type=circle
小方块: type=square
有序列表的格式如下:
<ol>
<li>列表内容1</li>
<li>列表内容2</li>
…
</ol>
有序列表同样可以设置type属性
数字:type=1,也是默认方式
英文字母:type=a或type=A
罗马数字:type=i或type=I
图片标签:
<img>
src=”3.gif” 图片来源,必写
alt=”小岳岳” 图片不显示时,显示的内容
title=”我的天呐” 鼠标移动到图片上时显示的文字
width=”600” 图片显示的宽度
height=”400” 图片显示的高度
超链接:
<a>
href="http://www.itcast.cn",前往地址,必填,注意要写http://
title="百度" 鼠标移动到链接上时显示的文字
target="_self"或者”_blank”,_self是默认值,在自身页面打开,_blank是新开页面前往连接地址:
<a href="http://www.baidu.com" title="百度" target="_self" >百度</a>
http超文本传输协议:
http请求消息:
分为四部分:
请求类型:http协议有很多种请求类型,最多的是get和post请求。
get:请求指定的页面信息,并返回实体主体
post:向指定资源提交数据进行处理请求。数据被包含在请求体中。post请求可能会导致新的资源的建立和/或已有资源的修改。
区别:post方式不显示提交的数据;get会显示,如用户名和密码之类的数据;使用post比get安全。
http响应消息:
响应消息是代表服务器收到请求消息后,给浏览器做的反馈,所以响应消息是服务器发送给浏览器的,响应消息分为四部分:
http常见状态码:
常见状态码:
web服务器:
使用http协议传送html文件,这只是应用层协议,我们需要一个传输层协议来完成我们的传输数据的工作,可选择TCP+HTTP
基于epoll的web服务器
//web服务端程序--使用epoll模型
#include
#include
#include
#include
#include
#include
#include
#include "pub.h"
#include "wrap.h"
int http_request(int cfd);
int main()
{
//改变当前进程的工作目录
char path[255] = {0};
sprintf(path, "%s/%s", getenv("HOME"), "webpath");
chdir(path);
//创建socket--设置端口复用---bind
int lfd = tcp4bind(9999, NULL);
//设置监听
Listen(lfd, 128);
//创建epoll树
int epfd = epoll_create(1024);
if(epfd<0)
{
perror("epoll_create error");
close(lfd);
return -1;
}
//将监听文件描述符lfd上树
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
int i;
int cfd;
int nready;
int sockfd;
struct epoll_event events[1024];
while(1)
{
//等待事件发生
nready = epoll_wait(epfd, events, 1024, -1);
if(nready<0)
{
if(errno==EINTR)
{
continue;
}
break;
}
for(i=0; i<nready; i++)
{
sockfd = events[i].data.fd;
//有客户端连接请求
if(sockfd==lfd)
{
//接受新的客户端连接
cfd = Accept(lfd, NULL, NULL);
//设置cfd为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
//将新的cfd上树
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else
{
//有客户端数据发来
http_request(cfd);
}
}
}
}
int send_header(int cfd, char *code, char *msg, char *fileType, int len)
{
char buf[1024] = {0};
sprintf(buf, "HTTP/1.1 %s %s\r\n", code, msg);
sprintf(buf+strlen(buf), "Content-Type:%s\r\n", fileType);
if(len>0)
{
sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);
}
strcat(buf, "\r\n");
Write(cfd, buf, strlen(buf));
return 0;
}
int send_file(int cfd, char *fileName)
{
//打开文件
int fd = open(fileName, O_RDONLY);
if(fd<0)
{
perror("open error");
return -1;
}
//循环读文件, 然后发送
int n;
char buf[1024];
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(fd, buf, sizeof(buf));
if(n<=0)
{
break;
}
else
{
Write(cfd, buf, n);
}
}
}
int http_request(int cfd)
{
int n;
char buf[1024];
//读取请求行数据, 分析出要请求的资源文件名
memset(buf, 0x00, sizeof(buf));
Readline(cfd, buf, sizeof(buf));
printf("buf==[%s]\n", buf);
//GET /hanzi.c HTTP/1.1
char reqType[16] = {0};
char fileName[255] = {0};
char protocal[16] = {0};
sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocal);
printf("[%s]\n", reqType);
printf("[%s]\n", fileName);
printf("[%s]\n", protocal);
char *pFile = fileName+1;
printf("[%s]\n", pFile);
//循环读取完剩余的数据
while((n=Readline(cfd, buf, sizeof(buf)))>0);
//判断文件是否存在
struct stat st;
if(stat(pFile, &st)<0)
{
printf("file not exist\n");
//发送头部信息
send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0);
//发送文件内容
send_file(cfd, "error.html");
}
else //若文件存在
{
//判断文件类型
//普通文件
if(S_ISREG(st.st_mode))
{
printf("file exist\n");
//发送头部信息
send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size);
//发送文件内容
send_file(cfd, pFile);
}
//目录文件
else if(S_ISDIR(st.st_mode))
{
}
}
}
web服务端开发:
//web服务端程序--使用epoll模型
#include
#include
#include
#include
#include
#include
#include
#include "pub.h"
#include "wrap.h"
int http_request(int cfd);
int main()
{
//若客户端请求文件较大,传输过程中客户端关闭,则服务端还在写入,此时会造成管道破裂
//设置信号,忽略SIGPIPE信号
struct sigaction act;
act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGPIPE,&act,NULL);
//改变当前进程的工作目录
char path[255] = {0};
sprintf(path, "%s/%s", getenv("HOME"), "webpath");
chdir(path);
//创建socket--设置端口复用---bind
int lfd = tcp4bind(9999, NULL);
//设置监听
Listen(lfd, 128);
//创建epoll树
int epfd = epoll_create(1024);
if(epfd<0)
{
perror("epoll_create error");
close(lfd);
return -1;
}
//将监听文件描述符lfd上树
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
int i;
int cfd;
int nready;
int sockfd;
struct epoll_event events[1024];
while(1)
{
//等待事件发生
nready = epoll_wait(epfd, events, 1024, -1);
if(nready<0)
{
if(errno==EINTR)
{
continue;
}
break;
}
for(i=0; i<nready; i++)
{
sockfd = events[i].data.fd;
//有客户端连接请求
if(sockfd==lfd)
{
//接受新的客户端连接
cfd = Accept(lfd, NULL, NULL);
//设置cfd为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
//将新的cfd上树
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else
{
//有客户端数据发来
http_request(cfd);
}
}
}
}
int send_header(int cfd, char *code, char *msg, char *fileType, int len)
{
char buf[1024] = {0};
//把格式化的数据写入某个字符串缓冲区。
sprintf(buf, "HTTP/1.1 %s %s\r\n", code, msg);
sprintf(buf+strlen(buf), "Content-Type:%s\r\n", fileType);
if(len>0)
{
sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);
}
strcat(buf, "\r\n");
Write(cfd, buf, strlen(buf));
return 0;
}
int send_file(int cfd, char *fileName)
{
//打开文件
int fd = open(fileName, O_RDONLY);
if(fd<0)
{
perror("open error");
return -1;
}
//循环读文件, 然后发送
int n;
char buf[1024];
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(fd, buf, sizeof(buf));
if(n<=0)
{
break;
}
else
{
Write(cfd, buf, n);
}
}
}
int http_request(int cfd)
{
int n;
char buf[1024];
//读取请求行数据, 分析出要请求的资源文件名
memset(buf, 0x00, sizeof(buf));
Readline(cfd, buf, sizeof(buf));
printf("buf==[%s]\n", buf);
//GET /hanzi.c HTTP/1.1
char reqType[16] = {0};
char fileName[255] = {0};
char protocal[16] = {0};
sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocal);
printf("[%s]\n", reqType);
printf("[%s]\n", fileName);
printf("[%s]\n", protocal);
char *pFile = fileName;
if(strlen(fileName)<=1){
strcpy(pFile,"./");
}else{
pFile = fileName+1;
}
//转换汉字编码
strdecode(pFile,pFile);
printf("pFile==[%s]\n",pFile);
//循环读取完剩余的数据,避免产生粘包
while((n=Readline(cfd, buf, sizeof(buf)))>0);
//判断文件是否存在
struct stat st;
if(stat(pFile, &st)<0)
{
printf("file not exist\n");
//发送头部信息
send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0);
//发送文件内容
send_file(cfd, "error.html");
}
else //若文件存在
{
//判断文件类型
//普通文件
if(S_ISREG(st.st_mode))
{
printf("file exist\n");
//发送头部信息
send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size);
//发送文件内容
send_file(cfd, pFile);
}
//目录文件
else if(S_ISDIR(st.st_mode))
{
printf("***********dir file******************\n");
//发送头部信息
send_header(cfd,"200","OK",get_mime_type(".html"),0);
//发送html文件头部
send_file(cfd,"html/dir_header.html");
char buffer[1024];
//文件列表信息
struct dirent **namelist;
int num;
//返回目录下满足条件的文件,并返回保存在namelist中
num = scandir(pFile, &namelist, NULL, alphasort);
if (num == -1) {
perror("scandir");
close(cfd);
epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);
return -1;
}else{
while (num--) {
printf("%s\n", namelist[num]->d_name);
memset(buffer,0x00,sizeof(buffer));
if(namelist[num]->d_type==DT_DIR){
//href=%s比下面多一个/
sprintf(buffer,"%s ",namelist[num]->d_name,namelist[num]->d_name);
}else{
sprintf(buffer,"%s ",namelist[num]->d_name,namelist[num]->d_name);
}
free(namelist[num]);
write(cfd,buffer,strlen(buffer));
}
free(namelist);
}
//发送html尾部
send_file(cfd,"html/dir_tail.html");
}
}
}