- 第一部分见博客
数据链路层协议-以太网帧协议-MAC地址封装
网络接口层协议-ARP协议
封装
- 传输层,把TCP/UDP的数据加到头部,再把应用数据一起加进来封装,其他层以此类推
分用
网络通信的过程
- 假如一台电脑的QQ要往另一个电脑的QQ发消息,应用层包含消息数据,还得包含QQ的协议头,这两进行封装,之后往下进行传输
- 假如使用UDP进行传输,需要有16位的源端口号,16位目的端口号,16位的UDP长度,16位的UDP检验和以及数据,所以就得在上层应用层的基础上加入这些
- 上一层封装的内容作为下一层的数据使用,网络层使用IP协议,要加入版本号,头部长度,IP地址(源端以及目的端)等数据需要加入,成了IP头+IP数据报的格式
- 再传递给数据链路层,经过以太网帧协议,加上MAC头,CRC校验等
- 看消息中的MAC地址与目的MAC地址是否一致,一致就接收,然后从下往上以此类推
- 根据MAC地址查找,那MAC地址怎么查找?使用的就是ARP协议
- ARP协议是根据IP找到MAC地址
- ARP报文占据28字节
- 还要加入以太网帧协议
- Q:请问arp广播时需不需要把IP地址也改成广播地址发送
- A:“主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。”上述中这个IP地址是ARP协议将要查找的IP地址,和发送的广播地址没有关系,不要搞混了
Socket通信
- 也是实现进间通信,但是需要在不同的主机
- socket是由IP地址和端口结合的,还包括协议栈的API
简介
- socket这就是端到端的通信,就无需考虑什么网络层,数据链路层之类的了,以前各个层之间传递,要加上层的数据头,现在socket直接把一整个直接包含进去了
- socket底层怎么解封装之类的没有管
字节序
- 现代CPU的累加器一次都能装载(至少)4字节(这里考虑32位机),即一个整数。那么这4字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。
- 字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)
- 字节序分为大端字节序(Big-Endian)和小端字节序(Little-Endian)。大端字节序是指一个整数的最高位字节(2331bit)存储在内存的低地址处,低位字节(07bt)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处
大端小端字节序
/*
字节序:字节在内存中存储的顺序。
小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
*/
// 通过代码检测当前主机的字节序
#include <stdio.h>
int main() {
union {
short value; // 2字节
char bytes[sizeof(short)]; // char[2]
} test;
test.value = 0x0102;
if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
printf("大端字节序\n");
} else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
printf("小端字节序\n");
} else {
printf("未知\n");
}
return 0;
}
- 小端字节序:数据的高位存在内存的高位
- 大端字节序:数据的高位存在内存的低位
字节序的转换
- 当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决问题的方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)
- 网络字节顺序是 TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式
- BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl
/*
网络通信时,需要将主机字节序转换成网络字节序(大端字节序),
(主机字节序可能是小端,,也可能是大端)
另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 - 网络字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 - 网络字节序
*/
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// htons 转换端口
unsigned short a = 0x0102;
printf("a : %x\n", a);
unsigned short b = htons(a);
printf("b : %x\n", b);
printf("=======================\n");
// htonl 转换IP
char buf[4] = {192, 168, 1, 100};
int num = *(int *)buf;
int sum = htonl(num);
unsigned char *p = (char *)∑
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
printf("=======================\n");
// ntohl
unsigned char buf1[4] = {1, 1, 168, 192};
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char *p1 = (unsigned char *)&sum1;
printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
// ntohs
return 0;
}
Socket地址
- // socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个socket地址。
- // 客户端->服务器(IP,Port)
通用Socket地址
- socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
#include <bits/socket.h> struct sockaddr {
sa_family_t sa_family;
char sa_data[14];//只用了6个字节来表示IPv4的地址数据,2+4,2位端口号,4位为IP,剩下8个空在那里
};
typedef unsigned short int sa_family_t;
-
sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocolfamily,也称domain)和对应的地址族入下所示:
| 协议族 | 地址族 | 描述 |
| — | — | — |
| PF_UNIX | AF_UNIX | UNIX本地域协议族 |
| PF_INET | AF_INET | TCP/IPv4协议族 |
| PF_INET6 | AF_INET6 | TCP/IPv6协议族 | -
宏 PF_*和 AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用
-
sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
| 协议族 | 地址值含义和长度 |
| — | — |
| PF_UNIX | 文件的路径名,长度可达到108字节 |
| PF_INET | 16bit端口号和32bitIPv4地址,共6字节 |
| PF_INET6 | 16bit端口号,32bit流标识,128bitIPv6地址,32bit范围ID,共26字节 | -
由上表可知,14字节的sa_data根本无法容纳多数协议族的地址值。因此,Linux定义了下面这个新的通用的socket地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的
#include <bits/socket.h> struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int ss_align;
char ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;
专用Socket地址
- 很多网络编程函数诞生早于IPv4协议,那时候都使用的是structsockaddr结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是 sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型
- UNIX本地域协议族使用如下专用的socket地址结构体:
#include <sys/un.h> struct sockaddr_un
{
sa_family_t sin_family; char sun_path[108];
};
- TCP/IP协议族有sockaddr_in和 sockaddr_in6两个专用的socket地址结构体,它们分别用于IPv4和IPv6:
- 所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr
IP地址转换(字符串IP-整数 ,主机、网络字节序的转换)
- IP地址采用的是点分十进制表示的
- 通常,人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的IP地址转化为可读的字符串。下面3个函数可用于用点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
- 下面这对更新的函数也能完成前面3个函数同样的功能,并且它们同时适用IPv4地址和IPv6地址:
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
//将点分十进制的IP地址转换为网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面,传出参数
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的IP的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
/*
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
*/
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// 创建一个ip字符串,点分十进制的IP地址字符串
char buf[] = "192.168.1.4";
unsigned int num = 0;
// 将点分十进制的IP字符串转换成网络字节序的整数
inet_pton(AF_INET, buf, &num);
unsigned char * p = (unsigned char *)#
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
// 将网络字节序的IP整数转换成点分十进制的IP字符串
char ip[16] = "";
const char * str = inet_ntop(AF_INET, &num, ip, 16);
printf("str : %s\n", str);
printf("ip : %s\n", str);
printf("%d\n", ip == str);
return 0;
}
TCP通信流程
// TCP通信的流程
// 服务器端(被动接受连接的角色)
- 创建一个用于监听的套接字(socket())
- 绑定IP和端口号(bind())
- 监听:监听有客户端的连接(listen())
- 套接字:这个套接字其实就是一个文件描述符
- 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口(accept())
- 设置监听,监听的fd开始工作,监听就是监听客户端读缓冲区中是否有数据
- 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
(fd)- 通信
- 接收数据
- 发送数据(发送数据到另一个主机的读缓冲区)
- 通信结束,断开连接
// 客户端
- 创建一个用于通信的套接字(fd)
- 连接服务器,需要指定连接的服务器的 IP和 端口
- 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
- 通信结束,断开连接
- 服务端当有客户端监听下来,会返回一个新的文件描述符,用于通信,不能用监听的fd来进行通信,如果用监听的文件描述符,那不就乱套了
- Q:没明白说的服务器端中为什么要将服务器端套接字的文件描述符与本地的IP和端口号绑定,而客户端却不需要?这是将本地作为服务器端了吗,客户端的IP地址如何指定的呢?
- A:客户端没有绑定的话系统会自动分配一个socket, 而服务端必须绑定,因为send()要求有目的地址作为参数。类似于打电话的时候不需要知道自己的号码,但一定知道对方的号码
套接字函数
客户端-服务器端通信
服务器端
// TCP 通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建socket(用于监听的套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 4.接收客户端连接
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端的信息
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", clientIP, clientPort);
// 5.通信
char recvBuf[1024] = {0};
while(1) {
// 获取客户端的数据
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == -1) {
perror("read");
exit(-1);
} else if(num > 0) {
printf("recv client data : %s\n", recvBuf);
} else if(num == 0) {
// 表示客户端断开连接
printf("clinet closed...");
break;
}
char * data = "hello,i am server";
// 给客户端发送数据
write(cfd, data, strlen(data));
}
// 关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
客户端
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recvBuf[1024] = {0};
while(1) {
char * data = "hello,i am client";
// 给客户端发送数据
write(fd, data , strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server data : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
}
// 关闭连接
close(fd);
return 0;
}
运行结果
- 调整每隔1秒发送数据
TCP三次握手
- TCP是一种面向连接的单播协议,三次握手是建立连接的时候协议的行为,不是程序员的行为,四次挥手也一样
- 三次握手发生在客户端连接上
- TCP是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP地址、端口号等
- TCP可以看成是一种字节流,它会处理 IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP头部
- TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用 四次挥手来关闭一个连接
- 三次握手可以确认客户端与服务端的收与发
- 怎么确定发送的数据是完整的呢?
- 发送的时候是一定的顺序发送的,那么怎么确保接收的顺序是一致的呢?
- 完整和顺序可以通过序号和确认号来查看
第一次握手:
1.客户端将SYN标志位置为1
2.生成一个随机的32位的序号seq=],这个序号后边是可以携带数据(数据的大小)
3.第一次握手是不能携带数据的
第二次握手:
1.服务器端接收客户端的连接:ACK=1
2.服务器会回发一个确认序号:ack=客户端的序号+数据长度+SYN/FIN(按一个字节算)
3.服务器端会向客户端发起连接请求:SYN=1
4.服务器会生成一个随机序号:seq=K
第三次握手:
1.客户单应答服务器的连接请求:ACK=1
2.客户端回复收到了服务器端的数据:ack=服务端的序号+数据长度+SYN/FIN(按一个字节算
TCP滑动窗口
- 滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)
- TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报。滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构
- 发送方的缓冲区,缓冲区理解为内存。白色格子就是空闲的空间;灰色格子就是,数据已经发送出去了,但是还没有被接收;紫色格子,还没有发送出去的数据
- 接收方的缓冲区,白色格子为空闲的空间;紫色格子就是已经接收到的数据
- 第一条语句的理解,其他语句以此类推:三次握手客户端发送一个SYN标志,0(0)表示随机序号是0,发送了0个数据,win4096表示滑动窗口是4096,表示缓冲区最多能接收4096个数据,mss1460表示一次报文段最大的数据是1460,win6144表示接收端的滑动窗口最大是6144,最大的一条数据是mss1024,之后回了个ACK 8001,为啥是ACK8001呢?因为你带的序号是8000,标志1,加1就是8001,其余以此类推
- 窗口理解为缓冲区的大小
- 滑动窗口的大小会随着发送数据和接收数据而变化
- 通信的双方都有发送缓冲区和接收数据的缓冲区
- 服务器:
- 发送缓冲区(发送缓冲区的窗口)
- 接收缓冲区(接收缓冲区的窗口)
- 客户端:
- 发送缓冲区(发送缓冲区的窗口)
- 接收缓冲区(接收缓冲区的窗口)
TCP四次挥手
- 在通信双方断开连接的时候,在程序中调用了close()会使用TCP协议进行四次挥手
- 客户端和服务端都可以主动发起断开连接,谁先调用close()谁就是发起
- 因为在TCP连接的时候
TCP通信-多进程实现并发服务器
- 要实现TCP通信服务器处理并发的任务,使用多线程或多进程来解决
- 思路:
- 1个父进程,多个子进程
- 父进程负责等待并接受客户端的连接
- 子进程:完成通信,接受一个客户端连接,就创建一个子进程用于通信
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recvBuf[1024];
int i = 0;
while(1) {
sprintf(recvBuf, "data : %d\n", i++);
// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
sleep(1);
}
// 关闭连接
close(fd);
return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
void recyleChild(int arg) {
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子进程都回收了
break;
}else if(ret == 0) {
// 还有子进程活着
break;
} else if(ret > 0){
// 被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}
int main() {
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 不断循环等待客户端连接
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
if(errno == EINTR) {
continue;
}
perror("accept");
exit(-1);
}
// 每一个连接进来,创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) {
// 子进程
// 获取客户端的信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(cfd, &recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(cfd, recvBuf, strlen(recvBuf) + 1);
}
close(cfd);
exit(0); // 退出当前子进程
}
}
close(lfd);
return 0;
}
- 不能在父进程中回收子进程,所以需要注册信号捕捉,用捕捉sigchld信号
TCP通信-多线程实现并发服务器
- 多线程共享同一个虚拟地址空间
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
struct sockInfo {
int fd; // 通信的文件描述符
struct sockaddr_in addr;//客户端信息
pthread_t tid; // 线程号
};
struct sockInfo sockinfos[128];
void * working(void * arg) {
// 子线程和客户端通信 cfd 客户端的信息 线程号
// 获取客户端的信息
struct sockInfo * pinfo = (struct sockInfo *)arg;
char cliIp[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
}
close(pinfo->fd);
return NULL;
}
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
bzero(&sockinfos[i], sizeof(sockinfos[i]));//初始化成0
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}
// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1) {
struct sockaddr_in cliaddr;//保存客户端接收到的信息
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
struct sockInfo * pinfo;
for(int i = 0; i < max; i++) {
// 从这个数组中找到一个可以用的sockInfo元素
if(sockinfos[i].fd == -1) {
pinfo = &sockinfos[i];
break;
}
if(i == max - 1) {
sleep(1);
i--;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr, &cliaddr, len);
// 创建子线程
pthread_create(&pinfo->tid, NULL, working, pinfo);
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}
TCP状态转换
- 三次握手由客户端先发起,其调用connect()函数主动连接服务器端,底层就开始进行第一次握手
半关闭
- 四次挥手后,客户端调用close(),会回一个ACK,如果服务端这边不调用close()就不会进行第三次挥手,如果服务端此时不调用close(),那么此时这个状态就是一个半关闭的状态,也就是只有一边关闭了,一端是不能发送数据的,只能接受数据,所以有这样的需求可以使用半关闭的状态
- 从程序的角度,可以使用API来控制实现半关闭的连接状态
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以 SHUT_WR。
- 使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写
- 注意:
- 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用进程都调用了 close,套接字将被释放
- 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。但如果一个进程 close(sfd) 将不会影响到其它进程
端口复用
参数:
- sockfd:要操作的文件描述符
- level:级别- SOL_SOCKET(端口复用的级别)
- optname:选项的名称
- SO_REUSEADDR
- SO_REUSEPORT
- optval:端口复用的值(整形)
- 1:可以复用
- 0:不可以复用
- optlen:optval参数的大小
- 端口复用,设置的时机是在服务器绑定端口之前
- 先调用setsockopt()函数设置端口复用
- 再用bind()进行绑定
常看网络相关信息的命令
netstat
参数:
-a所有的socket
-p显示正在使用socket的程序的名称
-直接使用P地址,而不通过域名服务器
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
while(1) {
char sendBuf[1024] = {0};
fgets(sendBuf, sizeof(sendBuf), stdin);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
}
close(fd);
return 0;
}
#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
//int optval = 1;
//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
// 绑定
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
return -1;
}
// 监听
ret = listen(lfd, 8);
if(ret == -1) {
perror("listen");
return -1;
}
// 接收客户端连接
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1) {
perror("accpet");
return -1;
}
// 获取客户端信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
// 输出客户端的信息
printf("client's ip is %s, and port is %d\n", cliIp, cliPort );
// 接收客户端发来的数据
char recvBuf[1024] = {0};
while(1) {
int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
if(len == -1) {
perror("recv");
return -1;
} else if(len == 0) {
printf("客户端已经断开连接...\n");
break;
} else if(len > 0) {
printf("read buf = %s\n", recvBuf);
}
// 小写转大写
for(int i = 0; i < len; ++i) {
recvBuf[i] = toupper(recvBuf[i]);
}
printf("after buf = %s\n", recvBuf);
// 大写字符串发给客户端
ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
if(ret == -1) {
perror("send");
return -1;
}
}
close(cfd);
close(lfd);
return 0;
}
- 为什么服务端有两个socket?一个是用来监听的,一个是用来通信的,所以两者的状态也不相同
- 当状态处于timewait的时候,会有两倍的报文等待时间
- 如果在1分钟之内没有把这个端口释放掉,那么就无法继续绑定
- 加上端口复用后,没有报错,能绑定上
I/O多路复用(I/O多路转接)
- 通信的双方都有SOCKET,对应内核当中的缓冲区,IO就是对缓冲区的操作,比如写就是从写缓冲区,写的时候就发送数据到另外一方的读缓冲区,读就是从读缓冲区读取数据
- I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll
- 假如有很多客户端给我发来消息,我不知道具体是哪个客户端发送的,是不是得进行遍历其文件描述符?对,这是之前的做法,一个个遍历文件描述符,而现在使用IO多路复用,则可以同时监听
- IO多路复用就是看文件描述符的读写缓冲区中是否有数据
I/O模型
阻塞等待
- read函数,get函数,accept(),recv()都是阻塞的,read一直在等数据,效率低
- 阻塞的时候不占用时间片
- 使用多线程多进程的方式来解决并发的问题
BIO模型
非阻塞,忙轮询
NIO模型
IO多路转接
- 委托内核完成
- 原先多个路,现在1路,原先看这个文件描述符有没有数据,需要挨个遍历,现在统一交给文件描述符,让他自己去遍历
select
- 主旨思想:
- 首先需要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中
- 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回:
a.这个函数是阻塞
b.函数对文件描述符的检测的操作是由内核完成的 - 在返回时,它会告诉进程有多少(哪些)描述符需要进行I/O操作
- 下面这个1024位,每一位代表一个文件描述符
- 检测读缓冲区看里面有没有数据,检测写缓冲区则是看里面有没有空余的空间,有的话才能往里面写数据,有空余数据的话把对应的标志位置为1
- 同理,读缓冲区的检测也是如此,将对应标志位置为1,然后检测的时候碰到有1的就去检测,为0的时候就不检测,只要1次调用就知道哪些文件描述符有数据
工作内容
- 关于select为什么要+1的原因解释,因为select从头开始遍历,从0到101,如果长度只为101,那么只能遍历到从0到100,所以需要+1
- reads是个指针传入传出参数,其监听到哪一位发生改变,比如监听到3,其位置为1了,用户态就可以遍历这个集合,遍历到3,有数据,就从3这个文件描述符去读数据,其余的没有数据就不用读了,这里用的是
fd_set reads
所以监听的是读缓冲区,检测的时候会把这个集合从用户态拷贝到内核态,再遍历集合,看哪一个需要检测,标志位为1就要检测,假如此时3,4都有数据,那么标志位保持为1不变,此时假如100,101没有数据,所以把标志位从1改成0,表示没有数据到达,然后从内核态拷贝到用户态,现在已经知道3,有数据,然后再遍历一遍,然后到3,就进行read、recv进行读数据了,判断标志位是否为1,可以用FD_ISSET(3,&reads)
进行判断 - 下一次循环,10对应的客户端结束了,断开了,就不会再让内核去检测了,就用
FD_CLR(100,&reads)
进行清除
代码实现
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 创建一个fd_set的集合,存放的是需要检测的文件描述符
fd_set rdset, tmp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1) {
tmp = rdset;
// 调用select系统函数,让内核帮检测哪些文件描述符有数据
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if(ret == -1) {
perror("select");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
if(FD_ISSET(lfd, &tmp)) {
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 将新的文件描述符加入到集合中
FD_SET(cfd, &rdset);
// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
for(int i = lfd + 1; i <= maxfd; i++) {
if(FD_ISSET(i, &tmp)) {
// 说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(i);
FD_CLR(i, &rdset);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
// sleep(1);
usleep(1000);
}
close(fd);
return 0;
}
- 通过select实现多客户端连接,这就是IO多路复用技术
poll
- 对select进行改进,将fds封装成了结构体数组,是一个需要检测文件描述符的集合
- 重点检测读事件
- 当既检测读事件又检测写事件时,
myfd.events = POLLIN | POLLOUT;
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 初始化检测的文件描述符数组
struct pollfd fds[1024];
for(int i = 0; i < 1024; i++) {
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;//要监听的文件描述符
int nfds = 0;
while(1) {
// 调用poll系统函数,让内核帮检测哪些文件描述符有数据
int ret = poll(fds, nfds + 1, -1);
if(ret == -1) {
perror("poll");
exit(-1);
} else if(ret == 0) {
continue;
} else if(ret > 0) {
// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
if(fds[0].revents & POLLIN) {
// 表示有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 将新的文件描述符加入到集合中
for(int i = 1; i < 1024; i++) {
if(fds[i].fd == -1) {
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
// 更新最大的文件描述符的索引
nfds = nfds > cfd ? nfds : cfd;
}
for(int i = 1; i <= nfds; i++) {
if(fds[i].revents & POLLIN) {
// 说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
close(fds[i].fd);
fds[i].fd = -1;
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
epoll
- 首先创建一个epoll实例,在内核区创建
- 可以通过文件描述符对内核区这个结构体进行操作
- 直接对内核进行操作,不用进行拷贝操作,不用用户态至内核态的用户切换
- rbr是红黑树的数据结构,取代了原先的线性数组结构,遍历速度加快,效率大大提高
- rdlist是双链表
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);//参数随便给一个值
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
//第一个是epfd,第二个是要操作的类型,第三个是要添加的文件描述符信息,第四个是event
struct epoll_event epevs[1024];//把发生改变的文件描述符信息装到这里,即内核检测完的数据,直接遍历就行
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);//-1表示阻塞
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);//返回的通信文件描述符,这不能和监听文件描述符混用
epev.events = EPOLLIN; //EPOLLIN | EPOLLOUT
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);//添加到实例当中
} else {//我们的逻辑是只有读检测到了事件,才能顺应接下来的逻辑,如果是写到来,则不适用
//EPOLLIN | EPOLLOUT,所以要加上这个判断,要根据返回来的events进行具体操作
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有数据到达,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);//关闭连接删除用NULL
close(curfd);
} else if(len > 0) {//说明读到数据了,然后把数据写出来
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
epoll的工作模式
- 水平触发是缺省的工作方式,默认的鹅工作方式,只要缓冲区有数据会一直通知
- 边沿触发只支持非阻塞,只通知一次
LT水平触发
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有数据到达,需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
- 从运行效果可以看出,只要缓冲区还有数据,这个ret就会一直更新
ET边沿触发
- 结合非阻塞的API来使用
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 设置cfd属性非阻塞
int flag = fcntl(cfd, F_GETFL);
flag | O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
epev.events = EPOLLIN | EPOLLET; // 设置边沿触发
epev.data.fd = cfd;//有数据只会通知一次
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 循环读取出所有数据
char buf[5];
int len = 0;
while( (len = read(curfd, buf, sizeof(buf))) > 0) {
// 打印数据
// printf("recv data : %s\n", buf);
write(STDOUT_FILENO, buf, len);
write(curfd, buf, len);
}
if(len == 0) {
printf("client closed....");
}else if(len == -1) {
if(errno == EAGAIN) {
printf("data over.....");
}else {
perror("read");
exit(-1);
}
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
- 由于边沿触发这个ret只会更新一次,所以弄完子循环后跳回while循环,会一直阻塞在
int ret = epoll_wait(epfd, epevs, 1024, -1);
当中,所以后来没有显示 - 改进后
Connection refused错误
- 这是由于2MSL造成的,要等1分钟
- 因为刚刚客户端阻塞在这,服务端先停止,谁先停止,谁就会有一个TIME_WAIT的状态,要等1分钟,之后再连接就行
UDP
- UDP是通过数据报来进行通信的,不需要保证数据的安全
- UDP这个Socket不像TCP的Socket那样,有监听又有通信,这直接是通信的套接字
UDP通信
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
// 2.绑定
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.通信
while(1) {
char recvbuf[128];
char ipbuf[16];
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接收数据
int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);
printf("client IP : %s, Port : %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(cliaddr.sin_port));
printf("client say : %s\n", recvbuf);
// 发送数据
sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
}
close(fd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 服务器的地址信息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);
int num = 0;
// 3.通信
while(1) {
// 发送数据
char sendBuf[128];
sprintf(sendBuf, "hello , i am client %d \n", num++);
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));
// 接收数据
int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
printf("server say : %s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
- UDP不需要多线程/多进程,就能有多个客户端与服务端进行通信
广播
- 用在局域网当中
- 发送一次,整个局域网的计算机都能接到消息
- 广播地址就是把主机地址的标志位全部置为1,也就是255
- 现在客户端也需要绑定服务器使用的端口,IP就拿广播地址使用
- 多做两件事,服务端设置广播属性,客户端绑定服务器广播数据的端口号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.设置广播属性
int op = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof(op));
// 3.创建一个广播的地址
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.193.255", &cliaddr.sin_addr.s_addr);
// 3.通信
int num = 0;
while(1) {
char sendBuf[128];
sprintf(sendBuf, "hello, client....%d\n", num++);
// 发送数据
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
printf("广播的数据:%s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
struct in_addr in;
// 2.客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.通信
while(1) {
char buf[128];
// 接收数据
int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("server say : %s\n", buf);
}
close(fd);
return 0;
}
- 客户端打开迟了,可能会丢失数据,不安全,如图,打开晚了从28开始接收
- 一个计算机不能重复绑定,但是可以重新在一个新的计算机中绑定,如图,两台不同的主机
组播(多播)
- 标识一组的IP接口
- 在广播上还要增加几步
- 服务端设置多播,客户端加入多播组
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.设置多播的属性,设置外出接口
struct in_addr imr_multiaddr;
// 初始化多播地址
inet_pton(AF_INET, "239.0.0.10", &imr_multiaddr.s_addr);
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof(imr_multiaddr));
// 3.初始化客户端的地址信息
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "239.0.0.10", &cliaddr.sin_addr.s_addr);
// 3.通信
int num = 0;
while(1) {
char sendBuf[128];
sprintf(sendBuf, "hello, client....%d\n", num++);
// 发送数据
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
printf("组播的数据:%s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
struct in_addr in;
// 2.客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
struct ip_mreq op;
inet_pton(AF_INET, "239.0.0.10", &op.imr_multiaddr.s_addr);
op.imr_interface.s_addr = INADDR_ANY;
// 加入到多播组
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof(op));
// 3.通信
while(1) {
char buf[128];
// 接收数据
int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("server say : %s\n", buf);
}
close(fd);
return 0;
}
- 两个主机都能收到
本地套接字通信
- 作用就是本地进程(一个主机中)的通信,上面的都叫网络套接字,即不同主机之间的进程通信
- 实现流程与网络套接字实现差不多,使用TCP
- 使用AF_LOCAL,本地的进行通信
- 本地的用sockaddr_un
- 本地套接字原理很像有名管道
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>
int main() {
unlink("server.sock");
// 1.创建监听的套接字
int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);//本地套接字用LOCAL
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "server.sock");//因为数组名是指针常量,是不能被修改的,所以用strcpy
int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 100);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 4.等待客户端连接
struct sockaddr_un cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
printf("client socket filename: %s\n", cliaddr.sun_path);
// 5.通信
while(1) {
char buf[128];
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv");
exit(-1);
} else if(len == 0) {
printf("client closed....\n");
break;
} else if(len > 0) {
printf("client say : %s\n", buf);
send(cfd, buf, len, 0);
}
}
close(cfd);
close(lfd);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>
int main() {
unlink("client.sock");
// 1.创建套接字
int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(cfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "client.sock");
int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.连接服务器
struct sockaddr_un seraddr;
seraddr.sun_family = AF_LOCAL;
strcpy(seraddr.sun_path, "server.sock");
ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 4.通信
int num = 0;
while(1) {
// 发送数据
char buf[128];
sprintf(buf, "hello, i am client %d\n", num++);
send(cfd, buf, strlen(buf) + 1, 0);
printf("client say : %s\n", buf);
// 接收数据
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv");
exit(-1);
} else if(len == 0) {
printf("server closed....\n");
break;
} else if(len > 0) {
printf("server say : %s\n", buf);
}
sleep(1);
}
close(cfd);
return 0;
}
- 会生成对应的伪文件
- 每次运行要先删除伪文件,再重新绑定
Web Server
阻塞/非阻塞,同步/异步
- 主要针对的是网络IO来说的
- 两个阶段
- 这里的sockfd是可以是阻塞的或者是非阻塞的
- EINTER、EAGAIN等那些不是错误,所以需要对这些进行判断
- buf是我们自己去读的,所以是同步
- 打个比方,买了机票,自己去机场打印登机牌,这就是同步;异步是机票出来了,机场快递给你机票,就是异步;异步效率更高,花的是别人的时间
- 在处理IO的时候,阻塞和非阻塞都是同步IO,只有使用了特殊的API才是异步IO
IO模型(IO多路复用的延伸)
- IO多路复用API是同步还是异步呢?用epoll只能检测到数据到达,具体读的操作(read、recv)都是同步的IO。记住一句话,在处理IO的时候,阻塞和非阻塞都是同步IO,只有使用了特殊的API才是异步IO,所以IO多路复用的API是同步的
阻塞(Blocking)
- 调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必
须等这个函数返回才能进行下一步动作,这是文件描述符的属性
非阻塞(Non-Blocking,NIO)
- 非阻塞等待,每隔一段时间就去检测I/O事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据errno区分这两种情况,对于accept、recv和send,事件未发生时,errno通常被设置成EAGAIN
IO复用
- Linux用select/poll/epoll函数实现IO复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数据可读或河写时,才真正调用IO操作函数
- IO复用的目的是可以一次检测多个客户端的事件(单进程当中)
信号驱动(Signal-Driven)
- Linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进
程收到SIGIO信号,然后处理IO事件
- 内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率
异步(Asynchronous)
- Linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序
Web服务器简介(网页服务器)
- 一个WebServer就是一个服务器软件(程序),或者是运行这个服务器软件的硬件(计算机)。其主要功能是通过HTTP协议与客户端(通常是浏览器(Browser))进行通信,来接收,存储,处理来自客户端的HTTP请求,并对其请求做出HTTP响应,返回给客户端其请求的内容(文件、网页等)或返回一个Error信息
- 通常用户使用Web浏览器与相应服务器进行通信。在浏览器中键入“域名”或“IP地址:端口号”,浏览器则先将你的域名解析成相应的 IP 地址或者直接根据你的IP地址向对应的 Web 服务器发送一个 HTTP 请求。这一过程首先要通过 TCP 协议的三次握手建立与目标 Web 服务器的连接,然后 HTTP 协议生成针对目标 Web 服务器的 HTTP 请求报文,通过 TCP、IP 等协议发送到目标 Web 服务器上
HTTP
- 超文本传输协议(HypertextTransferProtocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII形式给出;而消息内容则具有一个类似MIME的格式。HTTP是万维网的数据通信的基础
原理
- HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。HTTP协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方 法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据
- 以下是 HTTP 请求/响应的步骤:
- 客户端连接到 Web 服务器
一个HTTP客户端,通常是浏览器,与 Web 服务器的 HTTP 端口(默认为 80 )建立一个 TCP 套接字连接。例如,http://www.baidu.com。(URL) - 发送 HTTP 请求
通过 TCP 套接字,客户端向 Web 服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据 4 部分组成。 - 服务器接受请求并返回 HTTP 响应
Web 服务器解析请求,定位请求资源。服务器将资源复本写到 TCP 套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据 4 部分组成。 - 释放连接 TCP 连接
若 connection 模式为 close,则服务器主动关闭 TCP连接,客户端被动关闭连接,释放 TCP 连接;若connection 模式为 keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求; - 客户端浏览器解析 HTML 内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的 HTML 文档和文档的字符集。客户端浏览器读取响应数据 HTML,根据 HTML 的语法对其进行格式化,并在浏览器窗口中显示。
例如:在浏览器地址栏键入URL,按下回车之后会经历以下流程: - 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TCP 连接;
- 浏览器发出读取文件( URL 中域名后面部分对应的文件)的 HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
- 服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器;
- 释放 TCP 连接;
- 浏览器将该 HTML 文本并显示内容
- HTTP协议是基于TCP/IP协议之上的应用层协议,基于请求-响应的模式。HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有接收到请求之前不会发送响应
HTTP请求报文
- 可以通过浏览器看到请求报文
HTTP响应报文
HTTP请求
- HTTP/1.1 协议中共定义了八种方法(也叫“动作”)来以不同方式操作指定的资源:
- GET:向指定的资源发出“显示”请求。使用 GET 方法应该只用在读取数据,而不应当被用于产生“副作用”的操作中,例如在 Web Application 中。其中一个原因是 GET 可能会被网络蜘蛛等随意访
问。 - HEAD:与 GET 方法一样,都是向服务器发出指定资源的请求。只不过服务器将不传回资源的本文部分。它的好处在于,使用这个方法可以在不必传输全部内容的情况下,就可以获取其中“关于该资源的信息”(元信息或称元数据)。
- POST:向指定资源提交数据,请求服务器进行处理(例如提交表单或者上传文件)。数据被包含在请求本文中。这个请求可能会创建新的资源或修改现有资源,或二者皆有。
- PUT:向指定资源位置上传其最新内容。
- DELETE:请求服务器删除 Request-URI 所标识的资源。
- TRACE:回显服务器收到的请求,主要用于测试或诊断。
- OPTIONS:这个方法可使服务器传回该资源所支持的所有 HTTP 请求方法。用’*'来代替资源名称,向 Web 服务器发送 OPTIONS 请求,可以测试服务器功能是否正常运作。
- CONNECT:HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接(经由非加密的 HTTP 代理服务器)。
- 常用GET/POST
服务器编程基本框架
- 虽然服务器程序种类繁多,但其基本框架都一样,不同之处在于逻辑处理
模块 | 功能 |
---|---|
I/O处理单元 | 处理客户连接,读写网络数据 |
逻辑单元 | 业务进程或线程 |
网络存储单元 | 数据库、文件或缓存 |
请求队列 | 各单元之间的通信方式 |
- I/O处理单元是服务器管理客户连接的模块。它通常要完成以下工作:等待并接受新的客户连接,接收客户数据,将服务器响应数据返回给客户端。但是数据的收发不一定在I/O处理单元中执行,也可能在逻辑单元中执行,具体在何处执行取决于事件处理模式。
- 一个逻辑单元通常是一个进程或线程。它分析并处理客户数据,然后将结果传递给I/O处理单元或者直接发送给客户端(具体使用哪种方式取决于事件处理模式)。服务器通常拥有多个逻辑单元,以实现对多个客户任务的并发处理。
- 网络存储单元可以是数据库、缓存和文件,但不是必须的。
- 请求队列是各单元之间的通信方式的抽象。I/O处理单元接收到客户请求时,需要以某种方式通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件。请求队列通常被实现为池的一部分。(池:进程池与线程池)
两种高校的事件处理模式
- 服务器程序通常需要处理三类事件:I/O事件、信号及定时事件。有两种高效的事件处理模式:Reactor和Proactor,同步I/O模型通常用于实现Reactor模式,异步I/O模型通常用于实现Proactor模式。
Reactor模式
要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元),将 socket 可读可写事件放入请求队列,交给工作线程处理。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步 I/O(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是:
- 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
- 主线程调用 epoll_wait 等待 socket 上有数据可读。
- 当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll
内核事件表中注册该 socket 上的写就绪事件。 - 当主线程调用 epoll_wait 等待 socket 可写。
- 当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果。
Proactor模式
Proactor 模式将所有 I/O 操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻辑。使用异步 I/O 模型(以 aio_read 和 aio_write 为例)实现的 Proactor 模式的工作流程是:
- 主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例)。
- 主线程继续处理其他逻辑。
- 当 socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
- 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序。
- 主线程继续处理其他逻辑。
- 当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
- 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。
- 区别就是R模式中主线程只负责监听,P模式啥事都交给主线程来做
模拟Proactor模式
使用同步 I/O 方式模拟出 Proactor 模式。原理是:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一”完成事件“。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。
使用同步 I/O 模型(以 epoll_wait为例)模拟出的 Proactor 模式的工作流程如下:
- 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
- 主线程调用 epoll_wait 等待 socket 上有数据可读。
- 当 socket 上有数据可读时,epoll_wait 通知主线程。主线程从 socket 循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事件表中注册 socket 上的写就绪事件。
- 主线程调用 epoll_wait 等待 socket 可写。
- 当 socket 可写时,epoll_wait 通知主线程。主线程往 socket 上写入服务器处理客户请求的结果。
线程池
- 来一个客户端就创建一个线程这个方法不好
- 线程池是由服务器预先创建的一组子线程,线程池中的线程数量应该和CPU数量差不多。线程池中的所有子线程都运行着相同的代码。当有新的任务到来时,主线程将通过某种方式选择线程池中的某一个子线程来为之服务。相比与动态的创建子线程,选择一个已经存在的子线程的代价显然要小得多。至于主线程选择哪个子线程来为新任务服务,则有多种方式:
- 主线程使用某种算法来主动选择子线程。最简单、最常用的算法是随机算法和RoundRobin(轮流选取)算法,但更优秀、更智能的算法将使任务在各个工作线程中更均匀地分配,从而减轻服务器的整体压力。
- 主线程和所有子线程通过一个共享的工作队列来同步,子线程都睡眠在该工作队列上。当有新的任务到来时,主线程将任务添加到工作队列中。这将唤醒正在等待任务的子线程,不过只有一个子线程将获得新任务的”接管权“,它可以从工作队列中取出任务并执行之,而其他子线程将继续睡眠在工作队列上。
- 线程池的一般模型为:
线程池中的线程数量最直接的限制因素是中央处理器(CPU)的处理器(processors/cores)的数量
N:如果你的CPU是4-cores的,对于CPU密集型的任务(如视频剪辑等消耗CPU计算资源的任务)来说,那线程池中的线程数量最好也设置为4(或者+1防止其他因素造成的线程阻塞);对于IO密集型的任务,一般要多于CPU的核数,因为线程间竞争的不是CPU的计算资源而是IO,IO的处理一般较慢,多于cores数的线程将为CPU争取更多的任务,不至在线程处理IO的过程造成CPU空闲导致资源浪费。
- 空间换时间,浪费服务器的硬件资源,换取运行效率。
- 池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源。 当服务器进入正式运行阶段,开始处理客户请求的时候,如果它需要相关的资源,可以直接从池中获取,无需动态分配。
- 当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源
有限状态机
- 逻辑单元内部的一种高效编程方法:有限状态机(finitestatemachine)。有的应用层协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态,服务器可以根据它来编写相应的处理逻辑。如下是一种状态独立的有限状态机:
STATE_MACHINE( Package _pack )
{
PackageType _type = _pack.GetType(); switch( _type )
{
case type_A:
process_package_A( _pack ); break;
case type_B:
process_package_B( _pack ); break;
}
}
- 这是一个简单的有限状态机,只不过该状态机的每个状态都是相互独立的,即状态之间没有相互转移。状态之间的转移是需要状态机内部驱动,如下代码:
STATE_MACHINE()
{
State cur_State = type_A;
while( cur_State != type_C )
{
Package _pack = getNewPackage();
switch( cur_State )
{
case type_A:
process_package_state_A( _pack );
cur_State = type_B;
break;
case type_B:
process_package_state_B( _pack );
cur_State = type_C;
break;
}
}
}
- 该状态机包含三种状态:type_A、type_B和type_C,其中type_A是状态机的开始状态,type_C是状态机的结束状态。状态机的当前状态记录在cur_State变量中。在一趟循环过程中,状态机先通过 getNewPackage方法获得一个新的数据包,然后根据cur_State变量的值判断如何处理该数据包。数据包处理完之后,状态机通过给cur_State变量传递目标状态值来实现状态转移。那么当状态机进入下一趟循环时,它将执行新的状态对应的逻辑
EPOLLONESHOT事件
- 即使可以使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该 socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个socket的局面。一个socket连接在任一时刻都只被一个线程处理,可以使用epoll的EPOLLONESHOT事件实现。
- 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事 件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。但反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个 socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。
实现
- 线程池配合互斥锁一起使用
- 所以先创建一个互斥锁头文件(代码简单就把.h文件和操作文件放到一起了)
#ifndef LOCKER_H
#define LOCKER_H
#include <exception>..异常
#include <pthread.h>
#include <semaphore.h>
// 线程同步机制封装类
// 互斥锁类
class locker {
public:
locker() {//构造函数
if(pthread_mutex_init(&m_mutex, NULL) != 0) { //返回值不等于0就是出错了
throw std::exception();//抛出异常
}
}
~locker() { //析构函数
pthread_mutex_destroy(&m_mutex);
}
bool lock() {
return pthread_mutex_lock(&m_mutex) == 0;
}
bool unlock() {
return pthread_mutex_unlock(&m_mutex) == 0;
}
pthread_mutex_t *get()//获取互斥量
{
return &m_mutex;
}
private:
pthread_mutex_t m_mutex;
};
// 条件变量类,判断队列当中是否有数据
class cond {
public:
cond(){
if (pthread_cond_init(&m_cond, NULL) != 0) {
throw std::exception();
}
}
~cond() {
pthread_cond_destroy(&m_cond);
}
bool wait(pthread_mutex_t *m_mutex) {
int ret = 0;
ret = pthread_cond_wait(&m_cond, m_mutex);
return ret == 0;
}
bool timewait(pthread_mutex_t *m_mutex, struct timespec t) {
int ret = 0;
ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
return ret == 0;
}
bool signal() {
return pthread_cond_signal(&m_cond) == 0;
}
bool broadcast() {
return pthread_cond_broadcast(&m_cond) == 0;
}
private:
pthread_cond_t m_cond;
};
// 信号量类
class sem {
public:
sem() {
if( sem_init( &m_sem, 0, 0 ) != 0 ) {
throw std::exception();
}
}
sem(int num) {
if( sem_init( &m_sem, 0, num ) != 0 ) {
throw std::exception();
}
}
~sem() {
sem_destroy( &m_sem );
}
// 等待信号量
bool wait() {
return sem_wait( &m_sem ) == 0;
}
// 增加信号量
bool post() {
return sem_post( &m_sem ) == 0;
}
private:
sem_t m_sem;
};
#endif
- 之后建立线程池的类
#ifndef THREADPOOL_H
#define THREADPOOL_H
#include <list>
#include <cstdio>
#include <exception>
#include <pthread.h>
#include "locker.h"
// 线程池类,将它定义为模板类是为了代码复用,模板参数T是任务类
template<typename T>
class threadpool {
public:
/*thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量*/
threadpool(int thread_number = 8, int max_requests = 10000);//构造
~threadpool();//析构
bool append(T* request);//添加任务
private:
/*工作线程运行的函数,它不断从工作队列中取出任务并执行之*/
static void* worker(void* arg);
void run();
private:
// 线程的数量
int m_thread_number;
// 描述线程池的数组,大小为m_thread_number
pthread_t * m_threads;//动态创建数组
// 请求队列中最多允许的、等待处理的请求的数量
int m_max_requests;
// 请求队列
std::list< T* > m_workqueue;
// 保护请求队列的互斥锁
locker m_queuelocker;
// 是否有任务需要处理
sem m_queuestat;//状态
// 是否结束线程
bool m_stop;
};
template< typename T >//实现构造函数
threadpool< T >::threadpool(int thread_number, int max_requests) : //冒号后可以对成员进行初始化
m_thread_number(thread_number), m_max_requests(max_requests),
m_stop(false), m_threads(NULL) {
if((thread_number <= 0) || (max_requests <= 0) ) { //小于0就是错误值,抛出异常
throw std::exception();
}
m_threads = new pthread_t[m_thread_number]; //创建数组,new动态创建
if(!m_threads) {
throw std::exception();
}
// 创建 thread_number 个线程,并将他们设置为脱离线程。自己释放资源
for ( int i = 0; i < thread_number; ++i ) {
printf( "create the %dth thread\n", i);
if(pthread_create(m_threads + i, NULL, worker, this ) != 0) { //worker必须是静态函数,C中是全局函数
//第一个参数为地址
//不等于0代表出错
delete [] m_threads;
throw std::exception();
}
if( pthread_detach( m_threads[i] ) ) { //出错了线程分离
delete [] m_threads;
throw std::exception();
}
}
}
template< typename T >
threadpool< T >::~threadpool() { //析构函数
delete [] m_threads;
m_stop = true;
}
template< typename T > //添加事件
bool threadpool< T >::append( T* request )
{
// 操作工作队列时一定要加锁,因为它被所有线程共享。
m_queuelocker.lock();
if ( m_workqueue.size() > m_max_requests ) { //不能超过最大的请求
m_queuelocker.unlock();
return false;
}
m_workqueue.push_back(request);//追加
m_queuelocker.unlock();//解锁
m_queuestat.post();//信号量增加
return true;
}
template< typename T >
void* threadpool< T >::worker( void* arg ) //实现worker,但是可以使用this将参数传递过来
{
threadpool* pool = ( threadpool* )arg;
pool->run();//线程创建出来就执行
return pool;
}
template< typename T >
void threadpool< T >::run() { //实现run
while (!m_stop) { //一直循环
m_queuestat.wait();
m_queuelocker.lock();
if ( m_workqueue.empty() ) {
m_queuelocker.unlock();
continue;
}
T* request = m_workqueue.front();//取出第一个任务
m_workqueue.pop_front();//取了就删掉,删掉第一个
m_queuelocker.unlock();
if ( !request ) {
continue;
}
request->process();
}
}
#endif
- 编写socket相关的主文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include "locker.h"
#include "threadpool.h"
#include "http_conn.h"
#define MAX_FD 65536 // 最大的文件描述符个数
#define MAX_EVENT_NUMBER 10000 // 监听的最大的事件数量
// 添加文件描述符
extern void addfd( int epollfd, int fd, bool one_shot );//加入exteren这个就是为了在别的文件中也能用
extern void removefd( int epollfd, int fd );
void addsig(int sig, void( handler )(int)){ //添加信号捕捉
struct sigaction sa; //注册信号
memset( &sa, '\0', sizeof( sa ) );
sa.sa_handler = handler;
sigfillset( &sa.sa_mask );//设置临时阻塞信号集
assert( sigaction( sig, &sa, NULL ) != -1 );
}
int main( int argc, char* argv[] ) {
if( argc <= 1 ) {
printf( "usage: %s port_number\n", basename(argv[0]));
return 1;
}
int port = atoi( argv[1] ); //转换成整数
addsig( SIGPIPE, SIG_IGN );//信号处理,捕捉到了信号就忽略他
threadpool< http_conn >* pool = NULL; //任务是http连接的任务
try {
pool = new threadpool<http_conn>;//创建
} catch( ... ) {
return 1;
}
http_conn* users = new http_conn[ MAX_FD ];//最大用户数
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );//监听套接字
int ret = 0;
struct sockaddr_in address;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_family = AF_INET;
address.sin_port = htons( port );//网络字节序
// 端口复用,在绑定之前进行设置
int reuse = 1;//为1就是复用
//SOL_SOCKET是级别,SO_REUSEADDR代表端口复用
setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );//绑定
ret = listen( listenfd, 5 );//监听
// 创建epoll对象,和事件数组,添加,多路复用
epoll_event events[ MAX_EVENT_NUMBER ];//检测到了把事件写入数组
int epollfd = epoll_create( 5 );
// 添加到epoll对象中
addfd( epollfd, listenfd, false );//添加文件描述符
http_conn::m_epollfd = epollfd;//静态成员
while(true) { //主线程不断循环检测事件发生
int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 ); //检测到了几个事件
if ( ( number < 0 ) && ( errno != EINTR ) ) {
printf( "epoll failure\n" );
break;
}
for ( int i = 0; i < number; i++ ) { //循环遍历事件数组
int sockfd = events[i].data.fd;//获取监听的文件描述符
if( sockfd == listenfd ) {//有客户端连接进来
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
//连接客户端
if ( connfd < 0 ) {
printf( "errno is: %d\n", errno );
continue;
}
if( http_conn::m_user_count >= MAX_FD ) { //最大用户数不能超
close(connfd);
continue;
}
users[connfd].init( connfd, client_address);//直接把sockfd作为索引去操作
} else if( events[i].events & ( EPOLLRDHUP | EPOLLHUP | EPOLLERR ) ) {
//对方异常断开
users[sockfd].close_conn();//非监听fd的判断,即为通信fd
} else if(events[i].events & EPOLLIN) { //读的事件发生,模拟P模式一次性要把数据都读出来
if(users[sockfd].read()) { //一次性把所有数据读完
pool->append(users + sockfd);//交给工作线程处理,地址直接相加
} else {
users[sockfd].close_conn();//读失败了,关闭连接
}
} else if( events[i].events & EPOLLOUT ) { //检测写事件
if( !users[sockfd].write() ) {
users[sockfd].close_conn();
}
}
}
}
//程序结束
close( epollfd );
close( listenfd );
delete [] users;
delete pool;
return 0;
}
- 定义http头文件
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>
#include "locker.h"
#include <sys/uio.h>
class http_conn
{
public:
static const int FILENAME_LEN = 200; // 文件名的最大长度
static const int READ_BUFFER_SIZE = 2048; // 读缓冲区的大小
static const int WRITE_BUFFER_SIZE = 1024; // 写缓冲区的大小
// HTTP请求方法,这里只支持GET
enum METHOD {GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT};
/*
解析客户端请求时,主状态机的状态
CHECK_STATE_REQUESTLINE:当前正在分析请求行
CHECK_STATE_HEADER:当前正在分析头部字段
CHECK_STATE_CONTENT:当前正在解析请求体
*/
enum CHECK_STATE { CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT };
/*
服务器处理HTTP请求的可能结果,报文解析的结果
NO_REQUEST : 请求不完整,需要继续读取客户数据
GET_REQUEST : 表示获得了一个完成的客户请求
BAD_REQUEST : 表示客户请求语法错误
NO_RESOURCE : 表示服务器没有资源
FORBIDDEN_REQUEST : 表示客户对资源没有足够的访问权限
FILE_REQUEST : 文件请求,获取文件成功
INTERNAL_ERROR : 表示服务器内部错误
CLOSED_CONNECTION : 表示客户端已经关闭连接了
*/
enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };
// 从状态机的三种可能状态,即行的读取状态,分别表示
// 1.读取到一个完整的行 2.行出错 3.行数据尚且不完整
enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN };
public:
http_conn(){}//构造与析构
~http_conn(){}
public:
void init(int sockfd, const sockaddr_in& addr); // 初始化新接受的连接
void close_conn(); // 关闭连接
void process(); // 处理客户端请求,响应也在这里处理
bool read();// 非阻塞读
bool write();// 非阻塞写
private:
void init(); // 初始化连接
HTTP_CODE process_read(); // 解析HTTP请求
bool process_write( HTTP_CODE ret ); // 填充HTTP应答
// 下面这一组函数被process_read调用以分析HTTP请求
HTTP_CODE parse_request_line( char* text );
HTTP_CODE parse_headers( char* text );
HTTP_CODE parse_content( char* text );
HTTP_CODE do_request();
char* get_line() { return m_read_buf + m_start_line; }
LINE_STATUS parse_line();
// 这一组函数被process_write调用以填充HTTP应答。
void unmap();
bool add_response( const char* format, ... );
bool add_content( const char* content );
bool add_content_type();
bool add_status_line( int status, const char* title );
bool add_headers( int content_length );
bool add_content_length( int content_length );
bool add_linger();
bool add_blank_line();
public:
static int m_epollfd; // 所有socket上的事件都被注册到同一个epoll内核事件中,所以设置成静态的
static int m_user_count; // 统计用户的数量
private:
int m_sockfd; // 该HTTP连接的socket和对方的socket地址
sockaddr_in m_address; //通信socket地址
char m_read_buf[ READ_BUFFER_SIZE ]; // 读缓冲区
int m_read_idx; // 标识读缓冲区中已经读入的客户端数据的最后一个字节的下一个位置
int m_checked_idx; // 当前正在分析的字符在读缓冲区中的位置
int m_start_line; // 当前正在解析的行的起始位置
CHECK_STATE m_check_state; // 主状态机当前所处的状态
METHOD m_method; // 请求方法
char m_real_file[ FILENAME_LEN ]; // 客户请求的目标文件的完整路径,其内容等于 doc_root + m_url, doc_root是网站根目录
char* m_url; // 客户请求的目标文件的文件名
char* m_version; // HTTP协议版本号,我们仅支持HTTP1.1
char* m_host; // 主机名
int m_content_length; // HTTP请求的消息总长度
bool m_linger; // HTTP请求是否要求保持连接
char m_write_buf[ WRITE_BUFFER_SIZE ]; // 写缓冲区
int m_write_idx; // 写缓冲区中待发送的字节数
char* m_file_address; // 客户请求的目标文件被mmap到内存中的起始位置
struct stat m_file_stat; // 目标文件的状态。通过它我们可以判断文件是否存在、是否为目录、是否可读,并获取文件大小等信息
struct iovec m_iv[2]; // 我们将采用writev来执行写操作,所以定义下面两个成员,其中m_iv_count表示被写内存块的数量。
int m_iv_count;
};
#endif
- 源文件
#include "http_conn.h"
// 定义HTTP响应的一些状态信息
const char* ok_200_title = "OK";
const char* error_400_title = "Bad Request";
const char* error_400_form = "Your request has bad syntax or is inherently impossible to satisfy.\n";
const char* error_403_title = "Forbidden";
const char* error_403_form = "You do not have permission to get file from this server.\n";
const char* error_404_title = "Not Found";
const char* error_404_form = "The requested file was not found on this server.\n";
const char* error_500_title = "Internal Error";
const char* error_500_form = "There was an unusual problem serving the requested file.\n";
// 网站的根目录
const char* doc_root = "/home/kagome/webserver/resources";
int setnonblocking( int fd ) { //设置文件描述符非阻塞
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}
// 向epoll中添加需要监听的文件描述符
void addfd( int epollfd, int fd, bool one_shot ) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLRDHUP;//水平触发/边沿触发
if(one_shot)
{
// 防止同一个通信被不同的线程处理
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
// 设置文件描述符非阻塞
setnonblocking(fd); //ET模式要全给,非阻塞
}
// 从epoll中移除监听的文件描述符
void removefd( int epollfd, int fd ) {
epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, 0 );
close(fd);
}
// 修改文件描述符,重置socket上的EPOLLONESHOT事件,以确保下一次可读时,EPOLLIN事件能被触发
void modfd(int epollfd, int fd, int ev) {
epoll_event event;
event.data.fd = fd;
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}
// 所有的客户数
int http_conn::m_user_count = 0;
// 所有socket上的事件都被注册到同一个epoll内核事件中,所以设置成静态的
int http_conn::m_epollfd = -1;
// 关闭连接
void http_conn::close_conn() {
if(m_sockfd != -1) {
removefd(m_epollfd, m_sockfd);//删除
m_sockfd = -1;
m_user_count--; // 关闭一个连接,将客户总数量-1
}
}
// 初始化连接,外部调用初始化套接字地址
void http_conn::init(int sockfd, const sockaddr_in& addr){
m_sockfd = sockfd;
m_address = addr;
// 端口复用
int reuse = 1;
setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );
addfd( m_epollfd, sockfd, true);//添加到epoll,主线程只负责监听,其余交给http_conn
m_user_count++;
init();
}
void http_conn::init()
{
m_check_state = CHECK_STATE_REQUESTLINE; // 初始状态为检查请求行
m_linger = false; // 默认不保持链接 Connection : keep-alive保持连接
m_method = GET; // 默认请求方式为GET
m_url = 0;
m_version = 0;
m_content_length = 0;
m_host = 0;
m_start_line = 0;
m_checked_idx = 0;
m_read_idx = 0;
m_write_idx = 0;
bzero(m_read_buf, READ_BUFFER_SIZE);
bzero(m_write_buf, READ_BUFFER_SIZE);
bzero(m_real_file, FILENAME_LEN);
}
// 循环读取客户数据,直到无数据可读或者对方关闭连接
bool http_conn::read() {
if( m_read_idx >= READ_BUFFER_SIZE ) {
return false;
}
int bytes_read = 0;
while(true) {
// 从m_read_buf + m_read_idx索引出开始保存数据,大小是READ_BUFFER_SIZE - m_read_idx
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx,
READ_BUFFER_SIZE - m_read_idx, 0 );
if (bytes_read == -1) {
if( errno == EAGAIN || errno == EWOULDBLOCK ) {
// 没有数据
break;
}
return false;
} else if (bytes_read == 0) { // 对方关闭连接
return false;
}
m_read_idx += bytes_read;
}
return true;
}
// 解析一行,判断依据\r\n
http_conn::LINE_STATUS http_conn::parse_line() {
char temp;
for ( ; m_checked_idx < m_read_idx; ++m_checked_idx ) {
temp = m_read_buf[ m_checked_idx ];
if ( temp == '\r' ) {
if ( ( m_checked_idx + 1 ) == m_read_idx ) {
return LINE_OPEN;
} else if ( m_read_buf[ m_checked_idx + 1 ] == '\n' ) {
m_read_buf[ m_checked_idx++ ] = '\0';
m_read_buf[ m_checked_idx++ ] = '\0';
return LINE_OK;
}
return LINE_BAD;
} else if( temp == '\n' ) {
if( ( m_checked_idx > 1) && ( m_read_buf[ m_checked_idx - 1 ] == '\r' ) ) {
m_read_buf[ m_checked_idx-1 ] = '\0';
m_read_buf[ m_checked_idx++ ] = '\0';
return LINE_OK;
}
return LINE_BAD;
}
}
return LINE_OPEN;
}
// 解析HTTP请求行,获得请求方法,目标URL,以及HTTP版本号
http_conn::HTTP_CODE http_conn::parse_request_line(char* text) {
// GET /index.html HTTP/1.1
m_url = strpbrk(text, " \t"); // 判断第二个参数中的字符哪个在text中最先出现
if (! m_url) {
return BAD_REQUEST;
}
// GET\0/index.html HTTP/1.1
*m_url++ = '\0'; // 置位空字符,字符串结束符
char* method = text;
if ( strcasecmp(method, "GET") == 0 ) { // 忽略大小写比较
m_method = GET;
} else {
return BAD_REQUEST;
}
// /index.html HTTP/1.1
// 检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。
m_version = strpbrk( m_url, " \t" );
if (!m_version) {
return BAD_REQUEST;
}
*m_version++ = '\0';
if (strcasecmp( m_version, "HTTP/1.1") != 0 ) {
return BAD_REQUEST;
}
/**
* http://192.168.110.129:10000/index.html
*/
if (strncasecmp(m_url, "http://", 7) == 0 ) {
m_url += 7;
// 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
m_url = strchr( m_url, '/' );
}
if ( !m_url || m_url[0] != '/' ) {
return BAD_REQUEST;
}
m_check_state = CHECK_STATE_HEADER; // 检查状态变成检查头
return NO_REQUEST;
}
// 解析HTTP请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char* text) {
// 遇到空行,表示头部字段解析完毕
if( text[0] == '\0' ) {
// 如果HTTP请求有消息体,则还需要读取m_content_length字节的消息体,
// 状态机转移到CHECK_STATE_CONTENT状态
if ( m_content_length != 0 ) {
m_check_state = CHECK_STATE_CONTENT;
return NO_REQUEST;
}
// 否则说明我们已经得到了一个完整的HTTP请求
return GET_REQUEST;
} else if ( strncasecmp( text, "Connection:", 11 ) == 0 ) {
// 处理Connection 头部字段 Connection: keep-alive
text += 11;
text += strspn( text, " \t" );
if ( strcasecmp( text, "keep-alive" ) == 0 ) {
m_linger = true;
}
} else if ( strncasecmp( text, "Content-Length:", 15 ) == 0 ) {
// 处理Content-Length头部字段
text += 15;
text += strspn( text, " \t" );
m_content_length = atol(text);
} else if ( strncasecmp( text, "Host:", 5 ) == 0 ) {
// 处理Host头部字段
text += 5;
text += strspn( text, " \t" );
m_host = text;
} else {
printf( "oop! unknow header %s\n", text );
}
return NO_REQUEST;
}
// 我们没有真正解析HTTP请求的消息体,只是判断它是否被完整的读入了
http_conn::HTTP_CODE http_conn::parse_content( char* text ) {
if ( m_read_idx >= ( m_content_length + m_checked_idx ) )
{
text[ m_content_length ] = '\0';
return GET_REQUEST;
}
return NO_REQUEST;
}
// 主状态机,解析请求
http_conn::HTTP_CODE http_conn::process_read() {
LINE_STATUS line_status = LINE_OK;
HTTP_CODE ret = NO_REQUEST;
char* text = 0;
while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK))
|| ((line_status = parse_line()) == LINE_OK)) {
// 获取一行数据
text = get_line();
m_start_line = m_checked_idx;
printf( "got 1 http line: %s\n", text );
switch ( m_check_state ) {
case CHECK_STATE_REQUESTLINE: {
ret = parse_request_line( text );
if ( ret == BAD_REQUEST ) {
return BAD_REQUEST;
}
break;
}
case CHECK_STATE_HEADER: {
ret = parse_headers( text );
if ( ret == BAD_REQUEST ) {
return BAD_REQUEST;
} else if ( ret == GET_REQUEST ) {
return do_request();
}
break;
}
case CHECK_STATE_CONTENT: {
ret = parse_content( text );
if ( ret == GET_REQUEST ) {
return do_request();
}
line_status = LINE_OPEN;
break;
}
default: {
return INTERNAL_ERROR;
}
}
}
return NO_REQUEST;
}
// 当得到一个完整、正确的HTTP请求时,我们就分析目标文件的属性,
// 如果目标文件存在、对所有用户可读,且不是目录,则使用mmap将其
// 映射到内存地址m_file_address处,并告诉调用者获取文件成功
http_conn::HTTP_CODE http_conn::do_request()
{
// "/home/nowcoder/webserver/resources"
strcpy( m_real_file, doc_root );
int len = strlen( doc_root );
strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 );
// 获取m_real_file文件的相关的状态信息,-1失败,0成功
if ( stat( m_real_file, &m_file_stat ) < 0 ) {
return NO_RESOURCE;
}
// 判断访问权限
if ( ! ( m_file_stat.st_mode & S_IROTH ) ) {
return FORBIDDEN_REQUEST;
}
// 判断是否是目录
if ( S_ISDIR( m_file_stat.st_mode ) ) {
return BAD_REQUEST;
}
// 以只读方式打开文件
int fd = open( m_real_file, O_RDONLY );
// 创建内存映射
m_file_address = ( char* )mmap( 0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
close( fd );
return FILE_REQUEST;
}
// 对内存映射区执行munmap操作
void http_conn::unmap() {
if( m_file_address )
{
munmap( m_file_address, m_file_stat.st_size );
m_file_address = 0;
}
}
// 写HTTP响应
bool http_conn::write()
{
int temp = 0;
int bytes_have_send = 0; // 已经发送的字节
int bytes_to_send = m_write_idx;// 将要发送的字节 (m_write_idx)写缓冲区中待发送的字节数
if ( bytes_to_send == 0 ) {
// 将要发送的字节为0,这一次响应结束。
modfd( m_epollfd, m_sockfd, EPOLLIN );
init();
return true;
}
while(1) {
// 分散写
temp = writev(m_sockfd, m_iv, m_iv_count);
if ( temp <= -1 ) {
// 如果TCP写缓冲没有空间,则等待下一轮EPOLLOUT事件,虽然在此期间,
// 服务器无法立即接收到同一客户的下一个请求,但可以保证连接的完整性。
if( errno == EAGAIN ) {
modfd( m_epollfd, m_sockfd, EPOLLOUT );
return true;
}
unmap();
return false;
}
bytes_to_send -= temp;
bytes_have_send += temp;
if ( bytes_to_send <= bytes_have_send ) {
// 发送HTTP响应成功,根据HTTP请求中的Connection字段决定是否立即关闭连接
unmap();
if(m_linger) {
init();
modfd( m_epollfd, m_sockfd, EPOLLIN );
return true;
} else {
modfd( m_epollfd, m_sockfd, EPOLLIN );
return false;
}
}
}
}
// 往写缓冲中写入待发送的数据
bool http_conn::add_response( const char* format, ... ) {
if( m_write_idx >= WRITE_BUFFER_SIZE ) {
return false;
}
va_list arg_list;
va_start( arg_list, format );
int len = vsnprintf( m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list );
if( len >= ( WRITE_BUFFER_SIZE - 1 - m_write_idx ) ) {
return false;
}
m_write_idx += len;
va_end( arg_list );
return true;
}
bool http_conn::add_status_line( int status, const char* title ) {
return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title );
}
bool http_conn::add_headers(int content_len) {
add_content_length(content_len);
add_content_type();
add_linger();
add_blank_line();
}
bool http_conn::add_content_length(int content_len) {
return add_response( "Content-Length: %d\r\n", content_len );
}
bool http_conn::add_linger()
{
return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" );
}
bool http_conn::add_blank_line()
{
return add_response( "%s", "\r\n" );
}
bool http_conn::add_content( const char* content )
{
return add_response( "%s", content );
}
bool http_conn::add_content_type() {
return add_response("Content-Type:%s\r\n", "text/html");
}
// 根据服务器处理HTTP请求的结果,决定返回给客户端的内容
bool http_conn::process_write(HTTP_CODE ret) {
switch (ret)
{
case INTERNAL_ERROR:
add_status_line( 500, error_500_title );
add_headers( strlen( error_500_form ) );
if ( ! add_content( error_500_form ) ) {
return false;
}
break;
case BAD_REQUEST:
add_status_line( 400, error_400_title );
add_headers( strlen( error_400_form ) );
if ( ! add_content( error_400_form ) ) {
return false;
}
break;
case NO_RESOURCE:
add_status_line( 404, error_404_title );
add_headers( strlen( error_404_form ) );
if ( ! add_content( error_404_form ) ) {
return false;
}
break;
case FORBIDDEN_REQUEST:
add_status_line( 403, error_403_title );
add_headers(strlen( error_403_form));
if ( ! add_content( error_403_form ) ) {
return false;
}
break;
case FILE_REQUEST:
add_status_line(200, ok_200_title );
add_headers(m_file_stat.st_size);
m_iv[ 0 ].iov_base = m_write_buf;
m_iv[ 0 ].iov_len = m_write_idx;
m_iv[ 1 ].iov_base = m_file_address;
m_iv[ 1 ].iov_len = m_file_stat.st_size;
m_iv_count = 2;
return true;
default:
return false;
}
m_iv[ 0 ].iov_base = m_write_buf;
m_iv[ 0 ].iov_len = m_write_idx;
m_iv_count = 1;
return true;
}
// 由线程池中的工作线程调用,这是处理HTTP请求的入口函数
void http_conn::process() {
// 解析HTTP请求
HTTP_CODE read_ret = process_read();
if ( read_ret == NO_REQUEST ) {
modfd( m_epollfd, m_sockfd, EPOLLIN );
return;
}
// 生成响应
bool write_ret = process_write( read_ret );
if ( !write_ret ) {
close_conn();
}
modfd( m_epollfd, m_sockfd, EPOLLOUT);
}
- 实现
压力测试
- Webbench是 Linux上一款知名的、优秀的web性能压力测试工具。它是由Lionbridge公司开发。
测试处在相同硬件上,不同服务的性能以及不同硬件上同一个服务的运行状况。展示服务器的两项内容:每秒钟响应请求数和每秒钟传输数据量。
- 基本原理:Webbench首先fork出多个子进程,每个子进程都循环做web访问测试。子进程把访问的结果通过pipe 告诉父进程,父进程做最终的统计结果。
- 10000客户端,运行5秒成功