服务器开发最核心的部分有网络编程即网络IO管理
网络应用依赖于很多系统研究中已经学习过的概念,例如,进程、信号、字节顺序、存储器映射以及动态存储分配,接下来的文章主要内容为理解基本的客户端-服务器编程模型,实现编写一个web服务器。
客户端 - 服务器编程模型
俗称的cs架构,基本每个网络都是基于客户端-服务器模型(服务器管理资源,为客户端提供服务):
- 客户端向服务器发送一个请求,发起一个事务;
- 服务器接受到请求后,解析它,并操作它的资源;
- 服务器给客户端发送一个响应,等待下一个请求;
- 客户端收到响应并处理它;
这里的事务 不是常说的原子性概念上的事务,只是描述一次请求响应的执行步骤。
那么实现web服务器的具体流程就可描述为:客户端向服务器请求一个html文件,服务器响应请求后就读取一个磁盘文件,服务器将文件发送回客户端,客户端处理响应并由浏览器展示文件
网络 socket编程
客户端和服务器之间通过计算机网络的硬件和软件资源来通信,网络这部分内容主要讲解套接字(socket)。
一个套接字是连接的一个端点,一个连接即由两端的套接字地址唯一确定,形成套接字对(cliaddr:cliport, servaddr:servport)。
从unix程序的角度来看,套接字就是一个有相应描述符的打开文件。因此定义了函数open_clientfd封装了客户端socket的创建,定义了函数open_listentfd封装服务器socket的创建。
- socket函数
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
客户端和服务器使用socket函数来创建一个套接字描述符:
clientfd = socket(AF_INET, SOCK_STREAM, 0);
AF_INET表明使用因特网,SOCK_STREAM表示因特网连接的一个端点,返回:若成功则为非负描述符(表示获取到内核分配资源),出错则为-1. 该描述符仅是部分打开的,还不能读写
- connect 函数
#include <sys/socket.h>
int connect(int sockfd,struct sockaddr *serv_addr,int addrlen);
客户端通过connect来建议和服务器的连接,addrlen是sizeof(sockaddr_in) = 16字节。
connect会阻塞直到连接建立或者错误,如果成功,sockfd就准备好可以读写,并且得到的连接是由套接字对(x:y serv_addr.sin_addr:serv_addr.sin_port)刻画的,其中 x为客户端ip地址,y表示临时端口,由此确认客户端主机上的客户端进程
补充:
struct sockadd {
unsigned short sa_family; //protocol family
char sa_data[14];//address data
};
struct sockaddr_in {
unsigned short sin_family; //address family (always AF_INET)
unsigned short sin_port; //port number in network byte order
struct in_addr sin_addr; //ip address in network byte order
unsigned char sin_zero[8];//pad to sizeof(struct sockaddr)
};
- open_clientfd函数
将socket和connect函数封装起来调用会更方便
typedef struct sockaddr SA;
int open_clientfd(char *hostname, int port) {
int clientfd;
struct hostent *hp;
struct sockaddr_in serveraddr;
if ((clientfd = socket(AF_INET, SOCK_STREAM, 0) < 0) {
return -1;
}
if ( (hp = gethostbyname(hostname)) == NULL ) {
return -2;
}
//非标准c 可以用memset代替 #define bzero(b, len) (memset((b), '\0', (len)))
bzero((char*) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
//同memcpy, 但注意的是memcpy(dest, src, lien), 目的位置放置参数1, 而bcopy相反
bcopy((char*) hp->h_addr_list[0], (char*)&serveraddr.sin_addr.s_addr, hp->h_length);
//网络字节顺序为大端法 数据低位放置内存高地址
serveraddr.sin_port = htons(port);
if(connect(clientfd, (SA*) &serveraddr, sizeof(serveraddr)) < 0) {
return -1;
}
return clientfd;
}
通过gethostbyname检索服务器的DNS主机条目,并拷贝其第一个ip地址(已经是网络字节顺序),初始化服务器套接字地址结构后发起请求
以下为服务器端socket部分
4. bind函数
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
bind函数告诉内核my_addr中的服务器套接字地址和套接字描述符sockfd联系起来
- listen函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen将sockfd从一个主动套接字转化为监听套接字。backlog参数暗示了内核在开始拒绝连接请求之前,应该放入队列中等待的未完成连接请求的数量,属于tcp/ip协议的理解,通常设置 1024的值。
- open_listendfd函数
int open_listenfd(int port) {
int listenfd, optval=1;
struct sockaddr_in serveraddr;
if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return -1;
}
//eliminates "Address already in use" error from bind
//默认地, 一个重启的服务器将在大约30秒内拒绝客户端的连接请求
if ( setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int)) < 0 )
return -1;
//非标准c 可以用memset代替 #define bzero(b, len) (memset((b), '\0', (len)))
bzero((char*) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
//网络字节顺序为大端法 数据低位放置内存高地址
serveraddr.sin_port = htons((unsigned short)port);
if(bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0)
return -1;
if (listen(listenfd, LISTENQ) < 0)
return -1;
return listentfd;
}
设置INADDR_ANY 通配符地址告诉内核接受来自任何IP地址的请求
- accept函数
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
accept等待来自客户端的连接请求到达监听描述符listenfd,然后再addr中填写客户端的套接字地址, 并返回一个已连接描述符。服务器每次接受连接请求时都会创建一次连接描述符,用于表示客户端跟服务器之间连接的一次连接。这个概念将在建立并发服务器时讲述。