《Unix网络编程》卷1:套接字联网API(第3版):基本TCP编程、TCP客户端/服务器程序、I/O复用

全书共31章+附录。

计划安排
:吃透这本书,一天三章+源码,并实测代码做当天笔记,CSDN见。
时间安排:计划时间1.5个月 == 6个周末 ==  12天。
2017.08.05    第01-03章:TCP/IP简介、传输层、套接字编程简介
2017.08.06    第04-06章: 基本TCP编程、TCP客户端/服务器程序、I/O复用
2017.08.12    第07-09章:套接字选项、基本UDP编程、基本SCTP编程
2017.08.13    第10-12章:SCTP客户端/服务器程序例子、名字与地址互换、IPv4和IPv6互操作性
2017.08.19    第13-15章:守护进程和inetd超级服务器、高级I/O、Unix域协议
2017.08.20    第16-18章:非阻塞I/O、ioctl操作、路由套接字
2017.08.26    第19-21章:密钥管理套接字、广播、多播
2017.08.27    第22-24章:高级UDP编程、高级SCTP编程、带外数据
2017.09.02    第25-27章:信号驱动I/O、线程、IP选项
2017.09.03    第28-30章:原始套接字、数据链路访问、客户端/服务器程序设计范式
2017.09.09    第31章-附录:流。附录:IPv4/6协议、调试技术
2017.09.10    整理、总结:思维导图。

>>第4章丶基本TCP套接字编程


TCP客户端与服务器进程之间发生的一些典型事件的时间表:
《Unix网络编程》卷1:套接字联网API(第3版):基本TCP编程、TCP客户端/服务器程序、I/O复用_第1张图片
【图解】服务器首先启动,稍后某个时刻客户端启动,它试图连接到服务器。假设客户端给服务器发送一个请求,服务器处理该请求,并且给客户端发回一个响应。这个过程一直持续下去,直到客户关闭连接的客户端,从而给服务器发送一个EOF(文件结束)通知为止。服务器接着也关闭连接的服务器端,然后结束运行或者等待新的客户连接。

基本TCP套接字编程流程
为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型。
#include
int socket (int family, int type, int protocol);

TCP客户端使用connect函数来建立与TCP服务器的连接。
#include
int connect (int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

把一个本地协议地址赋予一个套接字,即绑定bind。
#include
int bind (int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

listen函数仅有TCP服务器调用,只做2件事:(1)监听哪个套接字;(2)套接字最大连接数。
#include
int listen (int sockfd, int backlog);

accept函数由TCP服务器调用,返回下一个已完成的连接。
#include
int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

/* 服务器端显示客户端的ip地址和端口号 */
#include 
#include "unp.h"

#define MAXLINE 4096
#define LISTENQ 1024
//#define SA struct sockaddr
typedef struct sockaddr SA;
typedef int socket_t; // 2017.08.06

int main(int argc, char **argv)
{
	int					listenfd, connfd;
	//struct sockaddr_in	servaddr;
	struct sockaddr_in	servaddr, cliaddr; // 2017.08.06
	socket_t			len; // 2017.08.06
	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(1300);	/* daytime server */

	bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

	listen(listenfd, LISTENQ);

	for ( ; ; ) {
		len = sizeof (cliaddr); // 2017.08.06
		connfd = accept(listenfd, (SA *)&cliaddr, &len); // 2017.08.06
		printf("connection from %s, port %d\n",
				inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
				ntohs(cliaddr.sin_port)); // 2017.08.06

        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        write(connfd, buff, strlen(buff));

		close(connfd);
	}
}
fork和exec

派生新的子进程:
#include
pid_t fork (void); // 在父进程中返回子进程的ID号,在子进程中返回0

调用新的程序进程:
#include
int execve (const char *filename,char *const argv[ ], char *const envp[ ]); //真正的系统调用 该函数,其他都是包装
int execl (const char *path,const char *arg,...);
int execlp (const char *file,const char *arg,...);
int execle (const char *path,const char *arg,..., char *const envp[ ]);
int execv (const char *path, char *const argv[ ]);
int execvp (const char *file, char *const argv[ ]);
int execvpe (const char *file, char *const argv[ ], char *const envp[ ]);

并发服务器

典型的并发服务器程序,使用fork一个子进程来服务每个客户。
/* 伪代码 */
pid_t pid;
int   listenfd, connfd;
listenfd = socket (...);
bind (listenfd, ...);
listen (listenfd, LISTENQ);
for (; ; ) {
    connfd = accept (listenfd, ...);
    if ((pid = fork()) == 0) {
        close (listenfd); /* child closes listening socket */
        /* do something */
        close (connfd);   /* done with this client */
        exit (0);
    }
    close (connfd);       /* parent closes connected socket */
}
本地和外地协议地址函数

#include
int getsockname (int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername (int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
1.在一个没有调用bind的TCP客户端上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号;
2.在以端口号0调用bind后,getsockname用于返回由内核赋予的本地端口号;
3.getsockname可用于获取某个套接字的地址族。
4.当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径便是调用getpeername。
/* 代码演示:获取套接字的地址族 */
int sockfd_to_family(int sockfd)
{
	struct sockaddr_storage ss;
	socklen_t	len;

	len = sizeof(ss);
	if (getsockname(sockfd, (SA *) &ss, &len) < 0)
		return(-1);
	return(ss.ss_family);
}
大多数TCP服务器是并发的,大多数UDP服务器是迭代的。

>>第5章丶TCP客户端和服务器程序示例-附:本书源码下载和编译


简单回射服务器:
《Unix网络编程》卷1:套接字联网API(第3版):基本TCP编程、TCP客户端/服务器程序、I/O复用_第2张图片
实际上此回射服务器构成了一个全双工的TCP连接。

/* 代码较多演示略 */
此处补充本书的源码下载地址: http://download.csdn.net/download/u014448505/7965427
下载完后通过虚拟机的共享文件夹靠背到linux系统中进行编译:
$ vi README
// README入手查看编译方法
$ ./configure && cd lib/ && make && cd ../libfree && make && cd ../libroute && make && cd ../libxti && make
// 配置和编译库接口文件
$ cd ../intro && make daytimetcpcli
// 发现 致命错误: net/if_dl.h:没有那个文件或目录  → 百度
$ sudo apt-get install apt-file
// [Y][Y]两步都是yes,安装完成后执行 apt-file -h 看是否出来帮助菜单来测试安装成功与否
$ apt-file update
// 更新完后继续
$ apt-file search if_dl.h
// libnewlib-dev: /usr/lib/newlib/i686-linux-gnu/include/net/if_dl.h  成功!回到编译
$ cd intro/ && make daytimetcpcli
// 成功编译。

本章示例程序编译、测试:
$ tcpserv01 &
[1] 12036
$ netstat -a
激活Internet连接 (服务器和已建立连接的)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 *:9877                  *:*                     LISTEN
$ tcpcli01 127.0.0.1
// 再开一个窗口
$ netstat -a | grep 9877
tcp        0      0 *:9877                  *:*                     LISTEN
tcp        0      0 localhost:9877          localhost:34143         ESTABLISHED
tcp        0      0 localhost:34143         localhost:9877          ESTABLISHED
// 回到刚才窗口
$ tcpcli01 127.0.0.1
hello
hello
bye bye
bye bye
^D ( 是终端的EOF字符)
/* 发送两个二进制整数给服务器的str_cli函数 */
void str_cli(FILE *fp, int sockfd)
{
	char			sendline[MAXLINE];
	struct args		args;
	struct result	result;

	while (Fgets(sendline, MAXLINE, fp) != NULL) {
        // sscanf 将两个参数从文本串转换为二进制数。
		if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) {
			printf("invalid input: %s", sendline);
			continue;
		}
		Writen(sockfd, &args, sizeof(args));

		if (Readn(sockfd, &result, sizeof(result)) == 0)
			err_quit("str_cli: server terminated prematurely");

		printf("%ld\n", result.sum);
	}
}

>>第6章丶I/O复用:select和poll


客户端阻塞于标准输入上的fgets调用期间,服务器进程会被杀死。
服务器TCP虽然正确的给客户端TCP发送了一个FIN,但是既然客户端进程正阻塞于从标准输入读入的过程,它将看不到这个EOF,知道从套接字读时为止,可能过了很长时间。
这样的 进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,也就是说输入已准备好被读取,或者描述符已经可以承接更多的输出,它就通知进程。
这个能力成为: I/O复用

I/O复用由 select和poll两个函数支持。
典型的网络应用场合:
1. 当客户端处理多个描述符时,通常是交互式输入和网络套接字,必须使用I/O复用。
2. 一个客户端同时处理多个套接字是可能的,比较少见。
3. 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用。
4. 如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。
5. 如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用。


UNIX下可用的5种I/O模型:
1. 阻塞式I/O;
2. 非阻塞式I/O;
3. I/O复用;
4. 信号驱动式I/O;
5. 异步I/O。
《Unix网络编程》卷1:套接字联网API(第3版):基本TCP编程、TCP客户端/服务器程序、I/O复用_第3张图片

select函数

该函数允许进程指示内核等待多个事件的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
事件类型:描述符的 读、写、异常、等待指定时间。(任何描述符都可以使用select)
#include
#include

int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
返回值:若有事件就绪的描述符则返回其数目,若超时则返回0,若出错则返回-1。

@ timeout
struct timeval {
    long tv_sec;
    long tv_usec;
};
1. 永远等下去:仅在有一个描述符准备好I/O时才返回;timeout == NULL;
2. 等待一段固定时间:在有一个描述符准备好I/O时返回;timeout == value;
3. 根本不等待:检查描述符后立即返回,称为轮询,必须指向一个timeout结构,但成员值必须为0。

@ readset / writeset / exceptset
让内核测试读、写、异常条件的描述符。
select使用描述符集,通常为整数数组,每个整数中的每一位对应一个描述符。
描述符集操作宏:
void FD_ZERO (fd_set *fdset);           /* clear all bits in fdset */
void FD_SET  (int fd, fd_set *fdset);   /* turn on the bit for fd in fdset */
void FD_CLR  (int fd, fd_set *fdset);   /* turn off the bit for fd in fdset */
int   FD_ISSET(int fd, fd_set *fdset);   /* is the bit for fd on in fdset? */
/* fd_set例子演示 - 伪代码 */
    fd_set rset;
    FD_ZERO (&rset);    /* initialize the set: all bits off */
    FD_SET  (1, &rset); /* turn on bit for fd 1 */
    FD_SET  (4, &rset); /* turn on bit for fd 4 */
    FD_SET  (5, &rset); /* turn on bit for fd 5 */
    // 定义了一个fd_set类型变量,然后打开描述符1/4和5的对应位。
此三个参数如果我们对其中某一个的条件不感兴趣,就可以设为NULL空指针。

@ maxfdp1
该参数指定待测试的描述符个数,值是待测试的最大描述符加1。
《Unix网络编程》卷1:套接字联网API(第3版):基本TCP编程、TCP客户端/服务器程序、I/O复用_第4张图片
shutdown函数

可以避免close函数两个问题:
1. close是把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。 shutdown可以不管引用计数问题直接激发TCP的正常连接终止序列
2. close终止读和写两个方向的数据传送。 shutdown可以实现单独关闭连接的读或者写端
#include
int shutdown (int sockfd, int howto);
@ howto
    SHUT_RD     关闭连接的读这一半
    SHUT_WR     关闭连接的写这一半
    SHUT_RDWR   连接的读半部和写半部都关闭
poll函数

与select功能类似,不过在处理流设备时,能够提供额外的信息。
#include
int poll (struct pollfd *fdarray, unsigned long nfds, int timeout);
struct pollfd {
    int     fd;
    short   events;
    short   revents;
};
// 在使用流设备时在做深入查看和学习。




2017.08.06
04-06章完成...

你可能感兴趣的:(Linux/Unix)