1.1 概述
要编写通过计算机网络通信的程序,首先要确定这些程序相互通信所用的协议。
一般认为Web服务器程序是一个长时间运行的程序(守护程序,daemon)它只在响应来自网络的请求时才发送网络消息。协议的另一端是Web客户程序,如某种浏览器,与服务器进程的通信总是由客户进程发起。
在设计网络应用时,确定总是由客户发起请求往往能够简化协议和程序本身。
1.2 一个简单的时间获取客户程序
下例是TCP当前时间查询客户程序的一个实现。该客户与其服务器建立一个TCP连接后,服务器以直观可读格式简单地送回当前时间和日期:
#include "unp.h"
int main(int argc,char* argv[])
{
int sockfd,n;
char recvline[MAXLINE+1];
struct sockaddr_in servaddr;
if(argc!=2)
err_quit("usage:a.out <IPaddress>");
if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
err_sys("socket error");
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(13); /*daytime server*/
if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<=0)
err_quit("inet_pton error for %s",argv[1]);
if(connect(sockfd,(SA*)&servaddr,sizeof(servaddr))<0)
err_sys("connect error");
while((n=read(sockfd,recvline,MAXLINE))>0)
{
recvline[n]=0;
if(fputs(recvline,stdout)==EOF)
err_sys("fputs error");
}
if(n<0)
err_sys("read error");
exit(0);
}
编译时需加上静态链接库,编译命令如下:
gcc daytimetcpcli.c -o daytimetcpcli -lunp
程序运行结果如下:
输入百度的ip,结果如下:
程序说明:
- socket函数创建了一个网际(
AF_INET
)字节流(SOCK_STREAM
)套接字。该函数返回一个小整数描述符,以后的所有函数调用(如connect
和read
)就用该描述符来标识这个套接字。 - 把服务器的IP地址和端口号填入一个网际 套接字地址结构(名为
servaddr
的sockaddr_in
结构变量)。使用bzero()
函数将整个结构清零后,置地址族为AF_INET
,端口号为13
(时间获取服务器的常用端口)。IP地址为第一个命令行参数的值。网际套接字地址结构中IP地址和端口号必须使用特定格式,因此调用库函数htons()
(主机到网络短整数)去转换二进制端口号,又调用库函数inet_pton()
(呈现形式到数值)去将ASCII命令行参数(IP地址)转换为合适的格式。 connect
函数应用于一个TCP套接字时,将与由它的第二个参数指向的套接字地址结构指定的服务器建立一个TCP连接。该套接字地址结构的长度也必须作为该函数的第三个参数指定。- 使用
read
函数读取服务器的应答。由于分段传输,数据可能被分成多个包,因此通常需要将read
编写在某个循环中,当read
返回0
(表示对端关闭连接)或负值
(表明发生错误)时终止循环。
1.5 一个简单的时间获取服务器程序
编写一个简单的TCP时间获取服务器程序,与前文的客户程序一道工作:
#include "unp.h"
#include <time.h>
int main(int argc,char* argv[])
{
int listenfd,connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
listenfd=socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(13);
Bind(listenfd,(SA*)&servaddr,sizeof(servaddr));
Listen(listenfd,LISTENQ);
for(;;)
{
connfd=Accept(listenfd,(SA*)NULL,NULL);
ticks=time(NULL);
snprintf(buff,sizeof(buff),"%.24s\r\n",ctime(&ticks));
Write(connfd,buff,strlen(buff));
Close(connfd);
}
}
程序说明
创建TCP套接字
11 TCP套接字的创建与客户程序相同
捆绑端口到套接字
12-17 填写一个网际套接字地址结构,调用bind
函数,端口13被捆绑到所创建的套接字。指定IP地址为INADDR_ANY
,如果服务器主机有多个网络接口,服务器进程就可以在任意网络接口上接受客户连接。
把套接字转换成监听套接字
18 调用listen函数将套接字转换为监听套接字。socket
、bind
、listen
这3个调用步骤是任何TCP服务器准备所谓的监听描述符(listenfd
)的正常步骤。
接受客户连接,发送应答
20-25 通常情况下,服务器进程在accept
调用中被投入睡眠,等待某个客户连接的到达并被内核接受。经过TCP三次握手后,accept
返回,其返回值是一个称为已连接描述符的新描述符(connfd
)。该描述符用于与新近连接的那个客户通信。accept
为每个连接到本服务器的客户返回一个新描述符。
终止连接
26 服务器通过调用close
关闭与客户的连接。该调用引发正常的TCP连接终止序列。
本服务器程序一次只能处理一个客户,如果多个客户连接差不多同时到达,系统内核在某个最大数目的限制下把它们排入队列,然后每次返回一个给accept
函数。