前言
本专栏将从零开始制作一个C++ Webserver,用以记录笔者学习的过程
如果你想要跟着我这个专栏制作一个C++ Webserver,你需要掌握以下前置基础课程知识:
1.C/C++的语法(在Leetcode刷100~200题的程度即可)
2.计算机网络基础知识
3.操作系统基础知识
掌握以上前置课程知识后,即可开始本专栏的内容
一.socket地址API
1.主机字节序和网络字节序
我们知道,一个32位计算机的CPU累加器一次能累加4字节的数据,而这4字节的数据在内存中排列的顺序是可以有2种方式的,即大端字节序和小端字节序
大端字节序:一个整数的高位字节(23~31bit)存储在内存的低地址处
小端字节序:一个整数的低位字节(0 ~ 7 bit)存储在内存的低地址处
由于不同的主机使用的字节序可能不同,因此两个主机之间发送数据可能会发生错误。解决方法是:
人们制定一个规范:发送端统一使用大端字节序(因此大端字节序称为网络字节序)
而因为现代PC大多采用小端字节序,因此小端字节序称为主机字节序
Linux中实现主机字节序和网络字节序的函数:
#include<netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
他们的作用可以通过名字理解,比如 htonl :" host to network long" ,即长整型的主机字节序转换成网络字节序。这四个函数中long类型的函数一般用来转换IP地址,short类型的一般用来转换端口号(操作系统基础知识)。
2.通用socket地址
主机之间的通信需要知晓对方的地址,而网络中主机的地址是TCP/IP协议族来定义的(计算机网络基础知识),在Linux网络编程中,我们通过使用socket的这个套接字来进行网络通信。socket定义了一系列的API实现网络通信,是非常方便好用的工具。
在socket网络编程中表示地址的是结构体sockaddr,但由于这个结构体的设计问题,无法容纳多数协议族的地址值,因此Linux定义了一个新的通用socket地址结构体
#include<bits/socket.h>
struct sockaddr_storge
{
sa_family_t sa_family;
unsigned long int __ss_align;
char__ss_padding[128-sizeof(__ss_align)];
}
3.专用socket地址
以上两种通用socket地址其实并不好用,所以Linux为各个协议族提供了专门的socket地址结构体
其中,UNIX本地协议族使用sockaddr_un,本文不予说明
而TCP/IP协议族使用sockaddr_in和sockaddr_in6两个分别对应IPv4和IPv6
但需要注意的是,使用sockaddr_in或其他专用socket地址(包括socket_storge),最后都要强制转换成通用socket地址类型sockaddr,这是因为所有socket编程接口使用的地址参数的类型都是sockaddr
二.创建socket
我们了解socket地址API后,要如何创建一个socket呢?下面是创建socket的代码
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type, int protocol);
int socket(int domain,int type,int protocol);
- 功能:创建一个套接字
- 参数:
- domain:协议族
- AF_INET:IPv4
- AF_INET6:IPv6
- AF_UNIX,AF_LOCAL:本地套接字通信
- type:通信过程中使用的协议类型
- SOCK_STEAM:流式协议(传输层使用TCP协议)
- SOCK_DGRAM:报式协议(传输层使用UDP协议)
- protoco:具体的一个协议,写 0 就行
- 返回值:返回文件描述符,操作内核缓冲区
- 失败:-1
以上块引用中,在写Webserver时都是用笔者加粗部分的参数
另外:
type参数中还可以与以下两个参数相与计算:
SOCK_NONBLOCK:创建的socket为非阻塞
SOCK_CLOEXEC:用fork调用创建子进程时,在子进程中关闭该socket
三.绑定socket(命名socket)
在创建socket时,我们在第一个参数时给它指定了协议族,但是并未指定使用协议族中哪个具体的socket地址。所以我们用系统调用bind来给socket绑定地址。
PS:服务端需要绑定,客户端不需要绑定,客户端采用匿名绑定,操作系统会代劳。
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen);
- 功能:将my_addr所指的socket地址分配给未命名的socket文件描述符
- 参数
- sockfd:通过socket函数得到文件描述符
- addr:需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen:第二个参数结构体占的内存大小
- 返回值
- 0:成功
- -1:失败
四.监听socket
现在我们创建好了socket,也为其分配好了socket地址,接下它就可以工作进行主机间的通信了吗?其实并不行,我们还需要为他创建一个监听队列,用以存储待处理的客户连接。
以下是创建监听队列的系统调用
#include<sys/socket.h>
int lsiten(int sockfd,int backlog);
int listen(int sockfd,int backlog);
- 功能:监听socket上的连接
- 参数
- sockfd:通过socket()函数得到文件描述符
- backlog:未连接的和已经连接的和的最大值
- 返回值:
- 成功:0
- 失败:-1
五.接受连接(服务端)
现在我们已经有了一个监听socket(执行过listen调用,处于LISTEN状态的socket),我们终于可以进行通信啦!而最后一步,就是将监听队列中的一个socket取出来,即可与远端的主机进行读写交互了
下面是从监听队列取出socket的系统调用
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr* addr,socklen_t* addrlen);
- 功能:接受客户端连接,默认时一个阻塞的函数,阻塞等待客户端连接
- 参数
- sockfd:用于监听的文件描述符
- addr:传出参数,记录连接成功后客户端的地址信息
- addrlen:指定第二个参数的对应的内存大小
- 返回值:
- 成功:返回用于通信的文件描述符
- 失败:-1
六.发起连接(客户端)
注意,上文的绑定socket,监听socket和接受连接都是服务端要干的事,接下来讲客户端的任务。在服务端创建好了监听队列后,就可以接受来自客户端的连接请求,那么客户端是怎么发起连接请求的呢?
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr* serv_addr,socklen_t addr_len);
- 功能:客户端连接服务器
- 参数
- sockfd:用于通信的文件描述符
- serv_addr:客户端要连接的服务器地址信息
- addrlen:指定第二个参数的对应的内存大小
- 返回值:
- 成功:0
- 失败:-1
七.关闭连接
通信完成后,如要关闭连接,可以通过下面的系统调用
#include<unistd.h>
int close(int fd);
fd参数是待关闭的socket,close系统调用不会立刻关闭一个连接,而是将fd的引用计数-1,只有当其引用计数为0时才会真正关闭连接(类似C++的智能指针)。
在多进程程序中,一次fork系统调用默认将引用计数+1.
如果你非要立即终止连接,也有办法,即shutdown系统调用,读者可以自行搜索。
八.数据读写
我们在二到七的过程中完整经历了socket通信的创建,命名,监听,接受(发起),关闭的过程,在连接建立成功到关闭连接的这个时间段中我们就可以进行两个主机之间的通信。
通信的方式即:
1.发送信息
2.接受信息
专业的说法也就是数据读写。Linux本身对文件的读写也可以用于socket,因为socket本身也是文件(Linux中万物皆文件)。但socket还是提供了几种好用的数据读写系统调用。
分别有
1.TCP数据读写
2.UDP数据读写
3.通用数据读写
本文只介绍TCP数据读写,UDP和通用数据读写请读者自行学习
#include<sys/types.h>
#include<sys/socket.h>
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);
- 功能:数据读写
- 参数:
- sockfd:用于通信的文件描述符
- buf: 缓冲区的位置
- len:缓冲区的大小
- flags:通常取0,其他含义自行搜素,可以进行具体的控制
- 返回值:
- 成功:
- recv:返回实际读到的数据的长度,如果小于期望长度len就多调用几次recv
- send:实际写入的数据长度
- 失败:-1
九.一些废话
前面一到八即Linux网络编程基础API的常用内容了,其他的一些不常用的API如地址信息函数,socket选项等没有写入文中,一是因为其使用场景较少,二是我对这个专栏的定位是:
简洁且重要
我写的内容基本上是完成C++ Webserver所必须掌握的前置知识,所以会极可能的少,这样的话对一些没有基础却又无从下手的后辈能起到一个引入门的作用。
等真正地了解了这些内容后,再去提升应该会容易的多。之所以这也想是因为我在学习完C/C++,操作系统,计算机网络之后进行项目制作时发现根本无从下手,我在网上找到的内容很少有建立在你完全不了解任何Webserver的知识,但又学完了基础课程的情况下。所以我希望能写一些内容,对和我一样的同学有些帮助。
另外就是我也是在学习过程中,所以想记录一下学习过程帮自己更好地完成这个目标。如果本文有什么问题,或者你有什么建议的话欢迎私信笔者,我看到了应该都会回复。