1 前言
自己一直想闹明白Unix网络编程到底是怎么一回事,所以开启这个系列文章。这个文章系列将会持续下去,直到不再Coding为止。
2 运行第一个实例
这篇博客是《Unix 网络编程卷一:套接字联网API》中第一章的内容的一个总结,主要是分析一个典型而又简单的socket程序,便于我们对socket编程有一个大概的认识,以便可以很快的进入主题。实现第一个"Hello World"总是快乐的,所以我们先看本书提供的"Hello World"程序- daytimetcpstc.c以及daytimetcpcli.c。
在www.unpbook.com中有本书所有的源码。下载之后解压会得unpv13e文件夹,阅读README发现需要先编译一下本书的环境,便于后面代码的使用。
根据README的提示开始编译本地主机编译环境:
(1)在解压的源码目录下,运行
./configure # try to figure out all implementation differences
cd lib # build the basic library that all programs need
make # use "gmake" everywhere on BSD/OS systems
cd ../libfree # continue building the basic library
make
cd ../libroute # only if your system supports 4.4BSD style routing sockets
make # only if your system supports 4.4BSD style routing sockets
cd ../libxti # only if your system supports XTI
make # only if your system supports XTI</span>
cd ../intro # build and test a basic client program
make daytimetcpsrv
sudo ./daytimetcpcli
make daytimetcpcli
./daytimetcpcli 127.0.0.1 就可以看到打印结果了:
Tue Sep 6 17:57:47 2016
也可以使用root权限运行,那样在执行应用程序的时候,就不会存在权限问题了。(我的主机环境是ubuntu12.04 LTS)
注意:(1)红色字体部分不用执行,因为我们不是在Unix环境下编译的,而是运行在linux系统上。
(2)如果在../libfree目录下执行make命令的时候,出现错误:
inet_ntop.c: 在函数‘inet_ntop’中:
inet_ntop.c:60:9: 错误: 实参‘size’与原型不符
/usr/include/arpa/inet.h:65:22: 错误: 原型声明
make: *** [inet_ntop.o] 错误 1
那么,只需将size_t size;改为socklen_t size;就可以了。
3 分析程序
3.1 客户端程序 - daytimetcpcli.c
#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; /* null terminate */
if (fputs(recvline, stdout) == EOF)
err_sys("fputs error");
}
if (n < 0)
err_sys("read error");
exit(0);
}
客户端的程序主要是完成与服务器端的绑定,然后读取从服务器传回来的数据。下面看看具体的实现过程:
line1是本书测试用列作者自己编写的头文件“unp.h”, 在./lib/unp.h中可以查看头文件的内容。其中line7中使用的MAXLINE也定义在这个头文件中,是4096.
line6 定义的sockfd用来作为line13中socket函数的返回值,这里称作整数描述符用来标识这个socket链接,line22的connect和line25的read函数都需要使用这个描述符。
line13创建了一个TCP套接字,学名为:“网际(AF_INET)字节流(SOCK_STREAM)” ,因为套接字的种类有很多包括UDP,SCTP等,这里只是用来标识TCP。
line16是将servaddr结构体清空。这里的bzero(void *ptr,size_t num)函数类似与memset(void *ptr, int value, size_t num)函数。是对内存的一段值进行替代。
line 17和18分别是复制socket类型以及端口号。htons设置二进制的端口号。
line19-20 使用inet_pton将输入的第二个参数以点分十进制的方式绑定到servadd的sin_addr字段。
line22-23 使用connect进行TCP连接,服务器的地址由servaddr决定。这里的SA是struct sockaddr的缩写形式,在"unp.h"中定义,这里是将某个特殊的套接字地址向通用型进行转换。
line25-29 客户端使用read函数将数据流写入recvline这个buffer中。这里的返回值n为recvline的当前指针的位置,如果服务器关闭那么返回值是负数或者0,read结束,然后
recvline[n]写入0;
line34 exit(0) 关闭所有进程。
line11,14,20,32这些是本书中自己定义的一些异常判断函数,这些函数封装起来主要是可以简化代码。
以上详细的分析了客户端程序实现的一些细节,概括来讲,除去前面的一些配置过程,客户端主要完成两个操作:connect和read
3.2 服务器端程序 - daytimetcpsrv.c
#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); /* daytime server */
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);
}
}
服务器端程序的功能是监听客户端的连接,然后将服务提供给客户端。下面看看具体的实现过程
line12是服务器端TCP套接字的创建,和客户端创建套接字的方式相同。只是返回值赋予的变量名称不同而已。同时注意到这里的Socket首字母用了大写,这里Socket是socket的包裹函数(封装),包含了对异常的处理。
line14-18用来给套接字地址结构赋值,注意这里的line16, address被限制为INADDR_ANY,表示可以接受任意地址的请求。
line18是Bind函数,用来将socket和address进行绑定。
line20通过调用Listen函数来监听套接字上的请求,一旦有客户端的请求到来转入内核进行处理。注意这里的LISTENQ是一个队列,如果同一时刻有多个请求到来那么将这些请求排队然后轮流进行处理。
line22-28是具体处理一个TCP连接的过程,当监听到一个客户端的请求后,通过三次握手,如果链接建立,那么accept函数会返回一个连接符,line24,25调用时间函数将当前的时间通过snprintf函数返回给buff, 然后调用Write函数将数据流写入connfd对应与客户端的read函数。
写好之后通过Close函数结束链接。
至此一个服务器的程序就介绍完了,总结起来,主要是三个部分:
socket建立,bind地址绑定,listen进行监听,连接建立之后进行处理。
4 一个简单的例子,却包含了网络编程的基本概念。让我门对网络编程有了粗略的认识。接下来,我们继续往下研究。