这篇博客是unix网络编程的第一篇,后续还会有一系列的文章。我之所以选择将这些记录在博客里面,是为了给自己看书以及工程经验的一个总结以及方便自己以后查找一些问题。之前看了好多前人们的博客,我自认为这种效果应该不错,虽然我一直认为写博客可能会花费太多时间,但是我想这中间花费更多的可能是我们总结的时间而非打字的时间。我想大部分同学入门网络编程可能都是靠自学,但是,如何提高看书的效果及效率,我想写点东西的时间花的总是值的,如果你也在犹豫到底要不要写点东西,那么我想别犹豫了试着写点什么吧,完事开头难,只要开始了第一步后面就容易多了,网络编程的学习也如此。
--------------------------------------------------------------------------------------------------------------------------------------------
这篇博客是《Unix 网络编程卷一:套接字联网API》中第一章的内容的一个总结,主要是分析一个典型而又简单的socket程序,便于我们对socket编程有一个大概的认识,以便可以很快的进入主题。实现第一个"Hello World"总是快乐的,所以我们先看本书提供的"Hello World"程序- daytimetcpstc.c以及daytimetcpcli.c。
在http://www.unpbook.com/src.html中有本书所有的源码。下载之后解压会得unpv13e文件夹,阅读README发现需要先编译一下本书的环境,便于后面代码的使用。
切换到root用户下执行:
# ./configure //在unpv13e当前目录下执行configure # cd lib //然后进入 lib目录,make一下编译环境 # make
前期准备个工作完成了。(注:本书所用系统是:ubuntu 13.10 虚拟机环境下网络拓扑)
然后在 ./intro/目录下有原始版本的 daytimetcpcli.c(client源码) 以及daytimetcpsrv.c (server端源码);
如果没有两台服务器的话,那么我们就在同一台服务器下面开两个进程(终端),其中一个作为server:
# cd intro/ # make daytimetcpsrv # ./daytimetcpsrv // 编译完之后,运行,server端就开始监听了。
一个进程作为client:
# cd intro/ # make daytimetcpcli # ./daytimetcpcli 127.0.0.1 //因为是在同一台主机上测试,所ip地址取127也可以取本地实际的地址,因为我的是在虚拟机下,所以192的ip也没什么意义。
然后就可以看到客户端以及将服务器端的server拿回了。至此一个简单而典型的socket程序就部署成功了。
(可能有的同学第一次接触socket不太明白,但是我们还是要提醒一下,要先启动server的程序然后再运行client端的)
至此 socket编程的Hello World就算完成了。下面我们开始进入程序的分析阶段,注意这里只是简单的给出一些函数的功能,至于其具体的说明以及实现的细节和参数等
在后面的博文中将会重点讲解。
-------------------------------------------------------------------------------------------------------------------------------------------
Daytimetcpcli.c
1 #include "unp.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 int sockfd, n; 7 char recvline[MAXLINE + 1]; 8 struct sockaddr_in servaddr; 9 10 if (argc != 2) 11 err_quit("usage: a.out <IPaddress>"); 12 13 if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 14 err_sys("socket error"); 15 16 bzero(&servaddr, sizeof(servaddr)); 17 servaddr.sin_family = AF_INET; 18 servaddr.sin_port = htons(13); /* daytime server */ 19 if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) 20 err_quit("inet_pton error for %s", argv[1]); 21 22 if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) 23 err_sys("connect error"); 24 25 while ( (n = read(sockfd, recvline, MAXLINE)) > 0) { 26 recvline[n] = 0; /* null terminate */ 27 if (fputs(recvline, stdout) == EOF) 28 err_sys("fputs error"); 29 } 30 if (n < 0) 31 err_sys("read error"); 32 33 exit(0); 34 }
客户端的程序主要是向服务器发送连接请求,建立连接之后,然后读取从服务器传回来的数据。下面看看具体的实现过程:
line1是本书测试用列作者自己编写的头文件“unp.h”, 在./lib/unp.h中可以查看头文件的内容。其中line7中哦功能的MAXLINE也定义在这个头文件中,是4096.
line6 定义的sockfd用来作为line13中socket函数的返回值,这里称作套接字的整数描述符用来标识这个socket链接,line22的connect和line25的read函数都需要使用这个描述符来对此socket进行操作。
line8 定义了一个sockaddr_in类型的结构体 servaddr用来标识将要访问的服务器的ip地址以及端口号等,sockaddr_in是ipv4的结构体类型,后面的博文会进行详细说明。
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分别是给服务器结构体的地址族和端口号进行赋值。这里使用的地址族是AF_INET,代表的是TCP类型的套接字,端口号是13,htons是(host to network short)的简称,他将主机字节序转换为网络中使用的网络字节序。
line19-20 使用inet_pton函数将输入的ip地址从我们机器上表示的ASCII字符串编码转换为套接字地址结构中的二进制值。后面的博文将会详细的介绍这个函数。
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
Daytimesrv.c
1 #include "unp.h" 2 #include <time.h> 3 4 int 5 main(int argc, char **argv) 6 { 7 int listenfd, connfd; 8 struct sockaddr_in servaddr; 9 char buff[MAXLINE]; 10 time_t ticks; 11 12 listenfd = Socket(AF_INET, SOCK_STREAM, 0); 13 14 bzero(&servaddr, sizeof(servaddr)); 15 servaddr.sin_family = AF_INET; 16 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 17 servaddr.sin_port = htons(13); /* daytime server */ 18 Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); 19 20 Listen(listenfd, LISTENQ); 21 22 for ( ; ; ) { 23 connfd = Accept(listenfd, (SA *) NULL, NULL); 24 ticks = time(NULL); 25 snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); 26 Write(connfd, buff, strlen(buff)); 27 Close(connfd); 28 } 29 }
服务器端程序的功能是监听客户端的连接,然后建立连接,之后将服务提供给客户端。下面看看具体的实现过程
line7 定义了两个变量"listenfd"和"connfd"分别代表line12服务器监听套接字以及line23已连接套接字。之所以对应不同的阶段是因为TCP创建连接时候使用的“三次握手协议,后面的博文会详细的讲到。
line 8 定义了一个类似于客户端程序中定义的ipv4套接字地址结构的结构体servaddr,这个套接字地址的结构是指定服务器监听的客户端的地址范围以及端口号。line14-18是对这个地址结构进行赋值,其中line16将地址类型设置为INADDR_ANY,表示可以接受任意地址的请求。line18将地址结构和监听套接字进行绑定。开始监听13号端口上来自任意ip地址的访问。
line12是服务器端监听套接字的创建,这里的参数表名创建的监听是一个AF_INET(TCP类型的)的SOCK_STREAM(字节流形式)。同时注意到这里的Socket首字母用了大写,这里Socket是socket的包裹函数(封装),包含了对异常的处理。
line20通过调用Listen函数来监听套接字上的请求,一旦有客户端的请求到来转入内核进行处理。注意这里的LISTENQ是一个队列,如果同一时刻有多个请求到来那么将这些请求排队然后轮流进行处理。
line22-28是具体处理一个TCP连接的过程,当监听到一个客户端的请求后,通过三次握手,如果链接建立,那么accept函数会返回一个连接符,line24,25调用时间函数将当前的时间通过snprintf函数返回给buff, 然后调用Write函数将数据流写入connfd对应与客户端的read函数。
写好之后通过Close函数结束链接。
至此一个服务器的程序就介绍完了,总结起来,主要是三个部分:
socket建立,bind地址绑定,listen进行监听,连接建立之后进行处理。
下面我们展开Richard老先生封装好的程序,主要是附件了一些头文件,将封装的函数进行替换,将一些常量或者别名重新进行声明。
我们在第三节具体讲解这些API的时候会附带讲解这些头文件。这里先不介绍了。
//#include "unp.h" #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #define MAXLINE 4096 #define PORT 13 typedef struct sockaddr SA; int main(int argc, char **argv) { int sockfd, n, counter = 0; char recvline[MAXLINE + 1]; struct sockaddr_in servaddr; if (argc != 2) { perror("usage: a.out <IPaddress>"); exit(1); } if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket error"); exit(1); } else { printf("socket created successfully!\n"); printf("socket id is: %d\n",sockfd); } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(PORT); /* daytime server */ inet_pton(AF_INET, argv[1], &servaddr.sin_addr); if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) { perror("connect error"); exit(1); } while ( (n = read(sockfd, recvline, MAXLINE)) > 0) { counter++; recvline[n] = 0; /* null terminate */ fputs(recvline, stdout); } if (n < 0) { perror("read error"); exit(1); } printf("counter = %d\n", counter); exit(0); }
//#include "unp.h" #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <time.h> #define MAXLINE 4096 #define PORT 13 #define SA struct sockaddr #define LISTENQ 5 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); printf("connected!\n"); ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); write(connfd, buff, strlen(buff)); close(connfd); } }
对于上面的程序只要使用gcc进行编译即可,然后可以在两个终端中进行运行测试。
-----------------------------------------------------------------------------------------------------------------------------------------
至此分别介绍完了 客户端以及服务器端的程序。虽然这两个程序很简单,但是却包含了几乎所有的socket编程都具备的几个步骤,所以熟悉这些步骤之后我们对socket编程就有了大概的概念,下面我们更加深入的进行学习,包括对TCP/IP协议栈的介绍以及socket编程特性。
2015/1/29 于南京 CSDN 转载请注http://blog.csdn.net/michael_kong_nju/article/details/43272195