[C++项目] Web Server(1):Socket 编程

概述

一个Web Server就是一个服务器软件(程序),或者是运行这个服务器软件的硬件(计算机)。其主要功能是通过HTTP协议与客户端(通常是浏览器(Browser))进行通信,来接收,存储,处理来自客户端的HTTP请求,并对其请求做出HTTP响应,返回给客户端其请求的内容(文件、网页等)或返回一个Error信息。

Web服务器端通过socket监听来自用户的请求。

远端的很多用户会尝试去connect()这个Web Server上正在listen的这个port,而监听到的这些连接会排队等待被accept()。由于用户连接请求是随机到达的异步事件,每当监听socket(listenfd)listen到新的客户连接并且放入监听队列,我们都需要告诉我们的Web服务器有连接来了,accept这个连接,并分配一个逻辑单元来处理这个用户请求。

服务器通过epoll这种I/O复用技术(还有selectpoll)来实现对监听socket(listenfd)和连接socket(客户请求)的同时监听。

注意I/O复用虽然可以同时监听多个文件描述符,但是它本身是阻塞的,并且当有多个文件描述符同时就绪的时候,如果不采取额外措施,程序则只能按顺序处理其中就绪的每一个文件描述符,所以为提高效率,我们将在这部分通过线程池来实现并发(多线程并发),为每个就绪的文件描述符分配一个逻辑单元(线程)来处理。

epoll_create()创建一个epoll实例。其中nfd为epoll句柄,参数max_size标识这个监听的数目最大有多大,从Linux 2.6.8开始,max_size参数将被忽略,但必须大于零。

epoll_create()返回引用新epoll实例的文件描述符。该文件描述符用于随后的所有对epoll的调用接口。每创建一个epoll句柄,会占用一个fd,因此当不再需要时,应使用close关闭epoll_create()返回的文件描述符,否则可能导致fd被耗尽。当所有文件描述符引用已关闭的epoll实例,内核将销毁该实例并释放关联的资源以供重用。

返回值:

成功时,这些系统调用将返回非负文件描述符。如果出错,则返回-1,并且将errno设置为指示错误。

1. 结构体

typedef union epoll_data {   
    void *ptr;   
    int fd;  
    __uint32_t u32;  
    __uint64_t u64;
}epoll_data_t;// 保存触发事件的某个文件描述符相关的数据

struct epoll_event {  
    __uint32_t events;  /* epoll event */   
    epoll_data_t data;  /* User data variable */
};

2. events

events:表示感兴趣的事件和被触发的事件,可取值如下:

EPOLLIN: 表示对应的文件描述符可以读;
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数可读;

EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: ET的epoll工作模式; 

3. epoll_ctl函数

声明:

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

功能:用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。

参数:

  • @epfd:由epoll_create生成的epoll专用的文件描述符;
  • @op:要进行的操作,EPOLL_CTL_ADD注册、EPOLL_CTL_MOD修改、EPOLL_CTL_DEL删除;
  • @fd:关联的文件描述符;
  • @event:指向epoll_event的指针;

linux下进程在PCB中的文件描述符表中来保存打开的文件 前三为默认标准流 最多总共打开1024个

socket

所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口, 是应用程序与网络协议根进行交互的接口。

在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。

与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。

区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

网络字节序

  • 大端字节序(Big Endian):最高有效位存于最低内存地址处,最低有效位存于最高内存处;
  • 小端字节序(Little Endian):最高有效位存于最高内存地址,最低有效位存于最低内存处。

网络字节序:大端字节序

UDP/TCP/IP协议规定: 把接收到的第一个字节当作高位字节看待

确定本机的字节序:

/* 确定你的电脑是大端字节序还是小端字节序 */ 
int check1() {  
    int i = 1; //1在内存中的表示: 0x00000001  
    char *pi = (char *)&i; //将int型的地址强制转换为char型  
    return *pi == 0; //如果读取到的第一个字节为1,则为小端法,为0,则为大端法 
} 

对字节序进行转换

#include<arpa/inet.h>  

//将主机字节序转换为网络字节序   host to network long/short   

// htonl 转ip  htons 转端口  剩下同理 

unit32_t htonl (unit32_t hostlong); 
unit16_t htons (unit16_t hostshort);  

//将网络字节序转换为主机字节序  

unit32_t ntohl (unit32_t netlong); 
unit16_t ntohs (unit16_t netshort); 

socket地址

struct sockaddr 通用地址 和 struct sockaddr_in 这两个结构体用来处理网络通信的地址。

struct sockaddr {  
    sa_family_t sin_family;//地址族  常用AF_INET AF_INET6 
    char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息  
};  

sockaddr 的缺陷是:sa_data把目标地址和端口信息混在一起了

sockaddr_in 解决了 sockaddr 的缺陷,把 portaddr 分开储存在两个变量中

struct sockaddr_in
{
    sa_family_t sin_family;  //地址族( Address Family )
    uint16_t sin_port;  //16位TCP/UDP端口号
    struct in_addr sin_addr; //32位IP地址
    char sin_zero[8]; //不使用
};

该结构体中提到的另一个结构体in_addr定义如下,它用来存放32位IP地址。

struct in_addr
{
    In_addr_t s_addr; //32位IPV4地址
};

In_addr_t 就是一个32位的无符号整数

为了保持前向兼容,使用时都需要强制转换成sockaddr*类型使用,使用时由于前16位都是保存的地址族,那么则可以读取地址族再根据类型进行强制类型转换,变为原来的地址结构体类型使用。

地址转换

通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。

但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。

in_addr_t inet_addr(const char *cp);//将IP地址从点十分格式转换成无符号长整型。 

char* inet_ntoa(struct in_addr in); //(“ntoa"的含义是"network to ascii”)  

int inet_aton(const char *cp, struct in_addr *inp);  //转换将点十分格式的ip地址为二进制并且为网络字节序,并存入指定的结构体中 

上面三个都是旧式的函数,一般现在用下面两个新函数

// p:点分十进制的IP字符串,n:表示network,网络字节序的整数 
int inet_pton(int af, const char *src, void *dst); 
  • af: 地址族: ipv4 AF_INET , ipv6 AF_INET6
  • src: 需要转换的点分十进制的IP字符串
  • dst: 转换后的结果保存在这个里面

inet_pton将点分十进制的IP地址字符串,转换成网络字节序的整数

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 
  • af:地址族: ipv4 AF_INET , ipv6 AF_INET6
  • src: 要转换的ip的整数的地址
  • dst: 转换成IP地址字符串保存的地方
  • size:第三个参数的大小(数组的大小)
  • 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

inet_ntop将网络字节序的整数,转换成点分十进制的IP地址字符串

tcp通信流程

服务器端

  1. 创建一个用于监听的套接字

    • 监听:监听有客户端的连接
    • 套接字:这个套接字其实就是一个文件描述符
  2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)

    • 客户端连接服务器的时候使用的就是这个IP和端口
  3. 设置监听,监听的fd开始工作

  4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,accept会得到一个和客户端通信的套接字(fd)

  5. 通信

    • 接收数据
    • 发送数据
  6. 通信结束,断开连接

客户端

  1. 创建一个用于通信的套接字(fd)
  2. 连接服务器,需要指定连接的服务器的 IP 和 端口
  3. 连接成功了,客户端可以直接和服务器通信
    • 接收数据
    • 发送数据
  4. 通信结束,断开连接

套接字相关函数

引入头文件

#include <sys/types.h> 
#include <sys/socket.h> 
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略 

创建socket

int socket(int domain, int type, int protocol); 
  • 功能:创建一个套接字
  • 参数:
  • domain: 协议族
    • AF_INET : ipv4
    • AF_INET6 : ipv6
    • AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
  • type: 通信过程中使用的协议类型
    • SOCK_STREAM : 流式协议
    • SOCK_DGRAM : 报式协议
  • protocol : 具体的一个协议。一般写0
    • SOCK_STREAM : 流式协议默认使用 TCP
    • SOCK_DGRAM : 报式协议默认使用 UDP
  • 返回值:
    • 成功:返回文件描述符,操作的就是内核缓冲区。
    • 失败:-1

绑定端口

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名 
  • 功能:绑定,将fd 和本地的IP + 端口进行绑定
  • 参数:
    • sockfd : 通过socket函数得到的文件描述符
    • addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
    • addrlen : 第二个参数结构体占的内存大小

监听端口

int listen(int sockfd, int backlog); 
// 系统中的最大连接数设置/proc/sys/net/core/somaxconn 
  • 功能:监听这个socket上的连接
  • 参数:
    • sockfd : 通过socket()函数得到的文件描述符
    • backlog : 未连接的和已经连接的和的最大值, 5

接收连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 
  • 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
  • 参数:
    • sockfd : 用于监听的文件描述符
    • addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
    • addrlen : 指定第二个参数的对应的内存大小
  • 返回值:
    • 成功 :用于通信的文件描述符
    • 失败 : -1

发起连接

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 
  • 功能: 客户端连接服务器
  • 参数:
    • sockfd : 用于通信的文件描述符
    • addr : 客户端要连接的服务器的地址信息
    • addrlen : 第二个参数的内存大小
  • 返回值:
    • 成功 : 0
    • 失败 : -1

读写数据

ssize_t write(int fd, const void *buf, size_t count); // 写数据 
ssize_t read(int fd, void *buf, size_t count); // 读数据