TCP客户/服务器程序设计模型-迭代服务器
本博客将从本文开始持续更新基于TCP的C/S程序设计基本模型系列文章。一开始我们以一个简单的C/S模型为例进行学习,在后续的文章中我们将给出不同的客户/服务器模型。
下面将要示例的程序具有这样的功能:客户程序与服务器建立一个TCP连接后,服务器以友好的方式将当前的时间和日期发送给客户程序。我们接下来重点对示例程序的设计模式和一些设计细节作以说明,程序中所涉及的socket接口具体可以参考这里的文章。
1.客户程序设计
在基于socket的C/S模型中,客户程序往往比服务器程序简单的多。客户程序通常完成的工作是:通过socket函数创建一个套接字;connect到服务器;如果连接成功,则与服务器进行数据交互,具体为从服务器读取数据或向服务器写入数据。客户程序的核心代码如下:
int main(int argc, char **argv) { int sockfd, n; char recvline[MAXLINE + 1]; struct sockaddr_in serv_addr; if (argc != 2) { printf("Usage:a.out \n"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) my_error("socket", errno, __LINE__); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(4555); if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0) my_error("inet_pton", errno, __LINE__); if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1) my_error("connect", errno, __LINE__); while ((n = read(sockfd, recvline, MAXLINE)) > 0) { recvline[n] = 0; if (fputs(recvline, stdout) == EOF) my_error("fputs", errno, __LINE__); } if (n < 0) printf("Error:read error\n"); exit(0); }
关于客户端程序的设计,说明如下:
1.客户端程序每次启动时,以命令行参数形式将服务器IP传递给客户端。因此程序一开始必须检测命令行参数的合理性;
2.由于我们的C/S模型是基于TCP的字节流套接字,因此相应的对socket函数使用AF_INET和SOCK_STREAM参数。socket函数返回一个整形的套接字描述符,在客户程序中要使用的connet和read等函数将依靠此描述符来对套接字进行操作;
3.在客户连接至服务器之前前,必须设置网际套接字地址结构,即依次对sockaddr_in结构中的字段填入正确的值。htons函数将主机字节序列转换成网络字节序列;inet_pton函数则是将字符形式的IP地址转换成二进制数值形式;
4.connect将与其第二个参数所指定的服务器建立一个TCP连接。connect函数的第二个参数为通用套接字地址结构sockaddr,因此必须对网际套接字地址结构进行强制类型转换;
5.通过套接字描述符sockfd从服务器端进行读数据,并通过fputs函数将读到的数据显示在标准输出设备;
2.服务器程序设计
服务器程序按照以下步骤进行设计:socket一个套接字;将服务器端口bind到服务器套接字上;通过listen函数将服务器套接字转换成监听套接字;accept客户端的连接;如果连接成功,则与客户端进行数据交互;终止连接。服务器程序核心程序如下所示。
int main(int argc, char **argv) { int listenfd, connfd; struct sockaddr_in serv_addr, client_addr; char buf[MAXLINE]; time_t ticks; int addr_size; if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) my_error("socket", errno, __LINE__); memset(&serv_addr, 0, sizeof(struct sockaddr_in)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(4555); if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1) my_error("bind", errno, __LINE__); if (listen(listenfd, LISTENQ) == -1) my_error("listen", errno, __LINE__); for (;;) { addr_size = sizeof(struct sockaddr); if ((connfd = accept(listenfd, (struct sockaddr*)&client_addr, &addr_size)) == -1) my_error("accept", errno, __LINE__); printf("%s has connectd to server..\n", inet_ntoa(client_addr.sin_addr)); ticks = time(NULL); snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks)); if (write(connfd, buf, strlen(buf)) < 0) my_error("write", errno, __LINE__); close(connfd); } }
关于服务器程序的相关细节,说明如下:
1.在设置网际套接字地址时,将IP地址指定为INADDR_ANY。如果服务器主机有多个网络接口,那么服务器进程就可以在任意网络接口上接受客户连接;
2.listen函数将套接字转换成监听套接字,这样内核就可以通过该套接字接受客户端的外来连接请求;
3.socket、bind和listen这三步是TCP服务器获得监听套接字描述符的通用步骤;
4.服务器的连续运转是通过死循环来实现的,并通过accept函数使得服务器程序不会过于占据系统内存。通常服务器进程通过accept函数进入阻塞状态,直到某个客户连接到达时才会被唤醒。客户和服务器之间的TCP连接是通过三次握手来建立的,accept函数建立连接成功后会返回另一个套接字描述符——已连接套接字。该套接字用于和刚刚连接的客户进行通信。
5.time函数将返回1970年1月1日至现在的秒数,而ctime函数将这些秒数转换成友好的时间格式;
6.wirte函数通过已连接套接字connfd将时间信息发送至客户端;
通过服务器程序我们可以看到,如果服务器没有处理客户端的请求,那么它将一直处于阻塞状态;如果服务器正在处理来自客户端的连接请求,那么其他请求连接至服务器的客户进程则一直处于阻塞状态。只有当服务器处理完当前客户的连接请求时,其他客户的连接请求才有机会被服务器处理。我们将此服务器称之为迭代式服务器,也就是说服务器对于每个客户都将迭代执行一次。
上述所示例的迭代服务器适用性很差,因为在服务器处理当前的客户请求之前,其余等待的客户程序都不能被处理。但它作为一个基本的C/S模型,对于初学者以及理解后续C/S模型都有极大帮助。