这一篇博客是继上次的如何写一个简单的Web Server(一)的第二篇博客,拖了久,一直没有写,上个周把套接字通信的代码重构了一下,这周就打算把挖的坑填完。
Socket
这是维基百科上对socket的定义,socket中文叫套接字,说实话我觉得这个翻译不是很好,没有接触之前无法将套接字与网络编程联系起来;
根据维基的定义,socket主要用于端对端的网络通信,两个端系统上的应用程序通过两个进程对组成,从一个进程向另一个进程发发送报文必须通过套接字,我们可以把进程想象成两座房子,而它们的套接字相当于它们的门,当一个主机向另一个主机发送报文时,它需要将报文推出门(套接字),该发送进程假设门与另一侧之间有运输的基础设施,当报文抵达目地主机时,它通过接收进程的门(套接字)传递,然后接收进程对报文进行处理。
多路复用和多路分解
在了解了套接字的定义后,自然而然的会想到在通信的过程中两个通信进程之间如何识别对方的问题,这就涉及到运输层的多路分解和多路复用;在端系统上运行的进程有一个或者多个进程,那么如何标示一个特定的套接字显然是一个问题,在接收主机中从运输层来的数据没有直接交付给进程,而是通过一个中间的套接字来传递。由于在任一时刻接收主机上不止一个套接字,所以每个套接字都有唯一的标识符,标识符的格式取决于它是UDP套接字还是TCP套接字。
现在考虑接收主机如何将一个收到的运输层报文定向到合适的套接字,为了达到这一目的,在每个传输层报文中设置了几个字段,在接收端运输层检查这些字段来标识出接收的套接字,然后将报文定向到对应的套接字。将运输层报文交付到正确的套接字的工作称为多路分解;从源主机的不同套接字中收集数据块,并为每个数据块封装上首部信息(将在多路分解上使用)从而生成报文段,然后将报文段传递到网络层的过程叫做多路复用。如下图所示:
现在我们理解了运输层多路复与读多路分解的作用,现在我们以UDP的多路复用与多路分解为例子来看看在主机中它们实际上是怎样工作的,通过先前的内容我们知道了运输层多路复用的要求:
1. socket要有唯一的标识符
2. 每个报文端有特殊的字段来指示该报文段所要交付的套接字(如下图所示这些特殊的字段是源端口号字段和目地端口字段)
如上图所示,端口号由一个16比特的的数字,其大小在065535之间,01023范围的端口号称为周知端口号,它们是保留给诸如HTTP(端口号80)和FTP(端口号21)之类的周知应用层协议的,当我们开发一个新的应用程序时,必须为其分配一个端口号。
当报文段到达主机时,运输层检查报文段中的目地端口号,并将其定向到相应的套接字,然后报文段的数据通过套接字进入其连接的进程。下面我们通过完成一个通信的程序来看看整个通信的过程。
这个程序分为client和server端,这里对server段的代码进行介绍,client的程序不做介绍,以下是整个过程的示意图:
首先,我们考虑,如果你需要和你写的server端的程序进行通信,你需要给server上的应用进程分配一个socket,并与其绑定,所以整个程序可以分为三步:
1. server创建套接字并与server绑定
2. server与client建立连接
3. server读取报文后关闭连接
首先我们来看看创建socket的程序:
//
// @Brief: Create a socket for communicate with client.
void Server :: CreateSocket()
{
socket_file_description_ = socket(AF_INET, SOCK_STREAM, 0);
error_handler_.CheckSocketCreatedOrNot(socket_file_description_);
}
其中socket()函数的是在sys/socket.h中声明的,其声明为:
int socket(int family, int type, int protocol)
若成功返回非负描述符,若出错则为-1
其中fanily参数指明了协议族, type参数指明socket类型,protocal参数可以设为下图中的某个常值,或者设为0,以选择所给定family和type组合的系统默认值,family、 type和protocol*的值如下图所示:
socket函数在成功时返回一个非负整数值,它与文件描述符类似,我们称它为套接字描述符,选择有效的type和protocol的组合来创建套接字,有效的组合如下图(部分组合):
创建好套接字后就需要将端口号和IP地址和套接字绑定,代码如下:
//
// @Brief: Set the server address, port number
void Server :: SetServerAddress()
{
bzero((char*)&server_address_, sizeof(server_address_));
// convert the port number from string of digits to an interger.
port_number_ = atoi(argv_[1]);
// must be AF_INET which contain a code for the address family.
server_address_.sin_family = AF_INET;
// INADDR_ANY will get the IP address on which server runs.
server_address_.sin_addr.s_addr = INADDR_ANY;
// convert the port numberin host byte order to network order.
server_address_.sin_port = htons(port_number_);
}
//
// @Brief: Bind the socket with server.
void Server :: BindSocketWithServer()
{
int bind_flag = bind(socket_file_description_, (sockaddr*)
(&server_address_), sizeof(server_address_));
error_handler_.CheckBindOrNot(bind_flag);
listen(socket_file_description_, 5);
}
上述代码中利用bind函数将一个本地协议地址赋予一个socket,对于IP协议,协议地址是32为的IPv4或者128位的IPv6地址与16位的TCP或者UDP端口号的组合,我们来看一看bind函数:
*int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen)
绑定成功返回0,若出错返回-1
bind函数中第二个参数是指向特定协议的地址结构结构指针,第三个参数是该地址的长度,对于TCP来说,调用bind函数可以指定一个端口号,或者一个IP地址,也可以两者都指定,也可以都不指定:
- 在服务器启动时会绑定他们众所周知端口,对于TCP客户或者服务器,如果bind的时候没有指定绑定端口,当调用connect或者listen的时候内核就会为相应的socket选择一个临时的端口。
- 进程可以把一个特定的IP地址绑定到它的socket上,不过这个IP地址必须属于其所在的主机的网络接口之一,对于TCP客户,这就为其指定了源IP地址。对于TCP服务器,这就限定该socket只接受目地IP为该服务器主机的客户连接。但是对于TCP客户机来一般不将IP地址绑定到其socket上,当客户机连接socket时,内核根据所用外出网络的的接口来选择源IP,而所用外出接口则取决于到达服务器所需的路径,如果TCP服务器没有把IP地址绑定到它的socket上,内核就把客户机发送的SYN的目地IP地址作为服务器的源IP地址。
调用bind的时候可以知道指定IP地址或者端口,可以两者都指定,也可以两者都不指定,如下图所示(根据结果来设置sin_addr和sin_port或者sin6_addr和sin6_port)
对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0,它告知内核去选择IP地址,如下面的语句:
struct sockaddr_in server_address;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
其中htonl是将主机序转换成网络序(如果需要查看更多关于这个函数的细节,请自行去查看这个函数的man page)
但是INADDR_ANY的值无论在主机序还是在网络序中,其值都是一样的,因此使用htonl并非必需的。
将端口或IP绑定好之后,server就需要在这个端口上监听连接了,监听需要用到listen函数,其声明如下:
int listen(int sockfd, int backlog)
若成功返回0,若出错返回-1
listen函数仅由TCP服务器调用,它做两件事:
- 当socket被创建时候,他默认被假设为一个主动socket,即它被默认是一个将要调用connect函数的客户socket,而listen函数将一个未连接的socket转换成一个被动socket,指示内核应接受指向该socket的连接请求,根据TCP状态转换图,服务器的连接是被动打开的,状态由CLOSED转移到LISTEN。
- 函数的第二个参数(backlog)规定了内核应该为相应socket排队的最大连接个数字;这里backlog参数我们做以下理解:
- 未完成连接队列,即处于SYN_RCVD的等待完成TCP三次握手过程的连接。
- 已完成连接队列,即已完成三次握手过程的连接,这些socket处于ESTABLISAHED状态。
以上这两个队列之和不超过backlog
剩下的就是client和server建立连接之后读写数据了,代码如下:
//
// @Brief: Establish connect with client.
void Server :: EstablishConnect()
{
client_length_ = sizeof(client_address_);
while(1)
{
//establish the connection
new_socket_file_description_ = accept(socket_file_description_,
(sockaddr*) &client_address_, &client_length_);
error_handler_.CheckAcceptOrNot(new_socket_file_description_);
pid_ = fork(); //create a new process to handle this connection
if(pid_ < 0)
error_handler_.ErrorMessageDisplay("Error on fork");
if(pid_ == 0)
{
close(socket_file_description_);
DisplayMessageFromClient();
exit(0); // the process exits
}else{ // the parent closes the new socket file description
close(new_socket_file_description_);
}
}
}
参考文献:
unix网络编程
Sockets Tutorial
Keep focus and have fun