MiniWebServer 的详细实现
在上面分析和设计的基础上,这一节介绍如何具体使用 C 语言来实现 MiniWebServer服务器。
5 监听和建立连接
按照网络编程的一般做法,我们采用套接字接口 Socket 来监听和建立网络连接。套接字( Socket)起初来源于 UNIX,是加利福尼亚大学 berkeley 分校为 BSD 操作系统( UNIX的一种)开发的网络通信接口,因此它也常被称为 berkeyley 套接字。随着 UNIX 操作系统的广泛使用,Socket 成为了最流行的网络通信程序接口,并被移植到 Window 和 JAVA 中,通常的网络编程都选择 socket 接口作为基础。
socket 接口本身被设计为可以使用与任何底层的协议,不过目前它的实现一般基于TCP/IP协议。TCP/IP 是互联网上最通用的网络协议 (尽管国际标准化组织规定了一个七层网络模型,但实际当中很少有完整的实现),它的基本模型如下图:
IP层负责将数据包从一个源节点传送到一个目标节点,节点有一个四字节的数字组成,即IP地址。网关收到数据包并根据该包目标IP来转换发到下一个网关,知道到目的地为止。
TCP层负责检查传输的数据包的正确性。数据包可能在网络上丢失,TCP负责检查数据包是否丢失或出错,然后请求重发数据包,知道获得正确而完整的数据包为止。而UDP则不对数据包的正确性进行检查。如果要保证数据的正确性,应用层应负责对此加以检查。
平时我们所用的 Socket接口正是在上述模型的基础上实现的。应用 Socket 接口编程时需要在服务器监听连接,而在客户端请求连接,建立连接的基本原理为:
- 服务器端:创建套接口-绑定套接口-设置套接口为监听模式,进入被动接受连接请求状态-接受请求,建立连接-度/写数据-终止连接
-
客户端: 创建套接口-与远程服务程序连接-读/写数据-终止连接
如果转换成 socket 接口,上面的过程可以完成:
- 服务器端:socket-->bind-->listen-->accept
-
客服端 : socket-->connect
下面来简单介绍在代码中需要用到的 socket 接口函数。
1.创建头接口 socket 函数
- int socket(int domain,int type,int protocol)
domain: 参数指定协议族,如AF_INET(IPv4协议),AF_INET6(IPv6协议) ,AF_LOCAL(UNIX域协议)
type :套接口类型,SOCK_STREAM:字节流套接口,SOCK_DGRAM数据报套接口 和SOCK_RAW 原始套接口。
其中,SOCK_STREAM,表示我们用的是TCP/IP协议,这样会提供按顺序的,可靠,双向,面向连接的比特流
SOCK_DGRAM,表示我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信。
potocol:参数指定协议,也可以取0
socket 函数成功时返回一个套接口文件描述符
2.绑定头接口 bind 函数
- int bind(int sockfd,struct sockaddr *my_addr,int addrlen)
sockfd: 表示由socket调用返回的文件描述符
addrlen: 就是 sizeof(sockaddr),套接字地址结构的长度
my_addr: 是一个指向包含有本机 IP 地址及端口号等信息的 sockaddr 类型的指针
sockaddr的定义如下:
- struct sockaddr{
- unsigned short as_family; //协议族
- char sa_data[14]; 14字节协议地址
- }
as_family:一般为AF_INET,代表 Internet(TCP/IP)地址族;
sa _data : 包含该socket的 IP地址和端口号
另外还有一种用于 Internet的套接字地址结构类型;
- struct sockaddr_in{
- short int sin_family //地址族
- unsigned short int sin_port; 端口号
- struct in_addr sin_addr; IP地址
- unsigned char sin_zero[8]; 填充0,以保持与 struct sockaddr 同样大小
- }
其中,sockaddr_in 的后缀_in表示internet。sin_zero用来将 sockaddr_in结构填充到与 struct sockaddr 同样的长度,可以用 bzero()或 memset() 函数将其置为零。
指向 sockadd_in 的指针可以指向 sockaddr 的指针可以相互转换,这意味着如果一个函数所需参数类型是 sockaddr 时,你可以在函数调用的时候将一个指向 sockaddr_in 的指针转换为指向 sockaddr 的指针;或者相反。
bind 函数返回值表明操作成功或失败,成功返回0,出错返回-1.
3.建立连接接口 connect 函数
面向连接的客户端程序使用 connect 函数来配置socket 并与远端服务器建立一个TCP连接,
- int connect(int sockfd,struct sockadr * serv_addr,int addrlen)
sockfd: 是socket函数返回的socket描述符,
serv_addr serv_addr是包含远端主机IP地址和端口号的指针
addrlen 是远端地址结构的长度
connect 函数在出现错误时返回-1,并且设置errno 为相应的错误码。
编写客户端无需调用 bind
4.监听接口 listen 函数
- int listen(int sockfd,int backlog)
sockfd: 是bind后的描述符
backlog:是为经过处理的连接请求队列可以容纳的最大数目。即每一个连入请求都要进入一个连入请求队列,等待listen的程序调用accept函数来接受这个连接。当系统还没有调用accept函数的时候,如果有很多连接,那么本地能够等待的最大数目就是backlog的数值
成功返回 0,出错返回-1
5.接受请求接口 accept函数
由TCP服务器端调用,用来接受从客户端来的请求,如果没有请求,则该函数自行阻塞i,直到有请求为止。
- int accept(int sockfd,struct sockaddr *sliaddr,socklen_t *addrlen)
sockfd: 是被监听的socket描述符,
cliaddr: 是一个指向sockaddr_in 变量的指针,该变量用来存放提出连接请求客户端信息
addrlen: 为一个指向值为 sizeof(struct socaddr_in)的整形指针变量
accept 成功时返回最后的服务器端的文件描述符,出现错误时,accept函数返回-1并置相应的errno值
在apue 上这么写着:
函数accept所返回的文件描述符是套接字描述符,该描述符连接到调用 connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字类型和地址族。传给accept的原始套接字灭有关联到这个连接,而是继续保持可用状态并接受其他连接请求。
6.关闭套接字接口 close 函数
7.字节转换函数
在网络上存在各种计算机,其字节顺序是不相同的,有大字节序和小字节序之分。网络传输过程中,应该统一使用一种字节序,即网络字节序。由各个发送端发送前将数据转换成网络字节序,而接收端对接收到的字节重新转换成本机字节序。在linux中有专门的字节转换函数。
- unsigned long int htonl(unsigned long int hostlong)
- unsigned short int htons(unsigned short int hostshort)
unsigned long int ntohl(unsigned long int netlong)
unisnged short int ntohs(unsigned short int netshort)
函数中 字母 h代表host,n代表network,s代表short,l代表long。第一个函数的意义是将本机器上的long数据转化为网络上的long,其他几个函数的含义相似。
8.设置socket描述符的状态
- int setsockopt(int s,int level,int optname,const void *optval,cocklen_toplen)
s: 所要设置的socket描述符
level: 所要改变的层数,通常使用SQL_SOCKET 代表socket的存取层
otname: 为所要设置的选项
optval: 代表要设置的值
optlen: 说明该值的长度
看看我们怎么实现 MiniWebServer 服务器中监听和建立连接部分的代码
完整代码链接- ...
- typedef struct sockaddr SA;
-
...
-
-
void handle_req(int fd);
-
int parse_uri(char *uri, char *filename, char *cgiargs);
-
void serve_static(int fd, char *filename, int filesize);
-
void get_filetype(char *filename, char *filetype);
-
void exec_cgi(int fd, char *method, int content_length, char *filename, char *cgiargs);
-
void error_msg(int fd, char *cause, char *errnum,
-
char *shortmsg, char *longmsg);
-
-
int main(int argc, char **argv)
-
{
-
/* Check command line args */
-
if (argc != 2) {
- ################################################
- fprintf格式化输出到文件,这里格式化输出到标准输出
- fprintf(stderr, "usage: %s \n", argv[0]);
-
exit(1);
-
}
-
-
(, SIG_IGN); ##### SIGCHLD子进程状态改变,SIG_IGN 表示忽略信号
-
signal(SIGPIPE, SIG_IGN);
-
-
int port = atoi(argv[1]); #获取端口号 8000
-
-
int listen_fd;
-
struct sockaddr_in client_addr;
-
struct sockaddr_in server_addr;
############# AF_INET IPv4 ,字节流接口
-
if((listen_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
-
perror("Unable to obtain network");
-
exit(1);
-
}
- ########################
-
###### 配置服务器,使它能立即终止或重启
- ###### SO_REUSEADDR:SO_REUSEADDR是让端口释放后立即就可以被再次使用
-
int optval = 1;
-
if((setsockopt(listen_fd, SOL_SOCKET, , (void *)&optval,
-
sizeof(int))) < 0) {
-
perror("setsockopt failed");
-
exit(1);
-
}
-
-
server_addr.sin_family = AF_INET; ###internet TCP/IP 地址族
-
server_addr.sin_port = htons(port); # 主机到 网络
-
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); ##define INADDR_ANY ((in_addr_t) 0x00000000)
-
-
if(bind(listen_fd, (SA *)&server_addr,
-
sizeof(server_addr)) < 0) {
-
perror("Unable to bind socket");
-
exit(1);
-
}
-
-
if(listen(listen_fd, 1024) < 0) {
-
perror("Unable to listen");
-
exit(1);
-
}
-
-
#ifdef DEBUG
-
printf("listening... s=%d \n", listen_fd);
-
#endif
-
-
int conn_fd, len;
-
while (1) {
-
len = sizeof(client_addr);
-
if((conn_fd = (listen_fd, (SA *)&client_addr, &len)) < 0) {
-
exit(1);
-
close(listen_fd);
-
}
-
#ifdef DEBUG
-
printf("connected. fd=%d \n", conn_fd);
-
#endif
-
handle_req(conn_fd); ######处理HTTP请求的处理
-
close(conn_fd);
-
}
-
}
-
阅读(2577) | 评论(0) | 转发(0) |