前言
以<深入理解计算机系统>(以下称“本书”)内容为基础,对程序的整个过程进行梳理。本书内容对整个计算机系统做了系统性导引,每部分内容都是单独的一门课.学习深度根据自己需要来定
引入
接续上一篇理解计算机系统_网络编程(4)_套接字api-CSDN博客
一个叫做echo的网络程序
本书P662给出了一个网络应用程序,由客户端程序和服务器端程序所组成.当连接建立起来后,用Unix I/O函数进行数据传输,因为前面没有系统研究过读写函数,看别人是怎么用的从中学习.
需求
本书原话:客户端在和服务器建立连接之后,客户端进入一个循环,反复从标准输入读取文本行,发送文本行给服务器,从服务器读取回送的行,并输出结果到标准输出. 当fgets在标准输入上遇到EOF时,或者因为用户在键盘上键入Ctrl+D,或者因为在一个重定向的输入文件中用尽所有的文本行时,循环终止.
---fgets函数在标准上遇到EOF和因为在一个重定向的输入文件中用尽所有的文本行,好像是一个意思,即表示到达文件末尾,返回NULL.Ctrl+D表示被中断,也返回NULL.
echo客户端代码
源码在本书P663,名称叫echoclient.c
代码精简的思考
首先源码是可以精简的,并不是对代码有质疑,他的写法读起来很清晰.如果自己写,在熟练的基础上可以"浓缩".
1>变量声明部分,去掉clientfd,host,port的声明
char buf[MAXLINE]; //表示缓冲字节数组
rio_t rio; //缓冲区对象
2>去掉13,14行,第16行的代码这样写:
int clientfd=Open_clientfd(argv[1],argv[2]);
两下一对比,源码表达的意思更清晰.所以还是不要精简比较好.
代码解读
第9到12行
客户端main函数有3个参数,如果数量不对则报错.
第13,14行
其中argv[1]表示传给main的第2个字符串参数,host---服务器主机名,即服务器域名
argv[2]表示传给main的第3个字符串参数,port---端口号
第16行
clientfd=Open_clientfd(host,port);
调用Open_clientfd函数,即open_clientfd的包装函数,建立和服务器的连接,得到可读写的文件描述符clientfd,具体见上一帖.
第17行
Rio_readinitb(&rio,clientfd);
调用rio_readinitb的包装函数Rio_readinitb.本书P628第1段讲了这个函数的含义,他是固定用法:每打开一个描述符,都会调用一次rio_readinitb函数.他将描述符fd和地址rp处的一个类型为rio_t的读缓冲区联系起来---黑体字是原话
====================================内容分割线↓============================
关于Unix I/O函数,本书在这一章节上排版比较乱,先大概有个理解,以后整理再做系统性的分析.
====================================内容分割线↑============================
第19到23行
while(Fgets(buf,MAXLINE,stdin)!=NULL){ //标准输入(键盘)→buf
Rio_writen(clientfd,buf,strlen(buf)); //buf→服务器端(通过clientfd)
Rio_readlineb(&rio,buf,MAXLINE); //服务器端→buf(通过&rio)
Fputs(buf,stdout); //buf→标准输出(屏幕)
}
----19行
Fgets函数表示从文件描述符stdin中,每次读取MAXLINE-1个字节到字符数组buf中.对应变量声明中的char buf[MAXLINE];也就是说buf接收stdin传来的数据.stdin是表示标准输入的文件描述符,整型表达是0.
Fgets的结束条件是:文件末尾EOF或者被中断.stdin是标准输入流,没有末尾,只有Ctrl+D可以让他结束(笔者个人理解).
----20行
Rio_writen是rio_writen的包装函数,含义在本书P627第6段中间:rio_wirten函数从位置usrbuf传送n个字节到描述符fd.---本书原话.因此这里的调用表示将buf数组中的数据传给描述符clientfd,结合向导图理解,数据发送给服务器端.结合第19行的代码,标准输入中的字符串发送到服务器端
---21行
Rio_readlineb是rio_readlineb的包装函数,含义在本书P628最后一段:rio_readlineb函数从文件rp读出下一个文本行(包括结尾的换行符),将他复制到内存位置usrbuf,并且用NULL(零)字符来结束这个文本行.---本书原话.&rio是读缓冲区,代表从服务器发来的数据,这里表示将数据写入buf中
---22行
Fputs是fputs函数的包装函数,表示将buf写入标准输出(屏幕),结合第21行,服务器端传来的数据被显示到屏幕上.
部分小结:代码注释了数据的走向,buf在数据传输的过程中起到了临时存储的作用.同时思考:前面提到了套接字是全双工的传输,但这里是不是没有表现出来?因为数据的进出都依赖于buf[MAXLINE].
第24行
Close(clientfd);
关闭文件描述符,本次连接结束.下一次连接需要重新运行客户端程序.
====================================内容分割线↓============================
笔者在这里做个理解:套接字连接是一个资源配对的过程,客户端和服务器端各自分配的资源---端口port,而后抽象出clientfd和listenfd描述符,这是双方连接准备阶段做的事.当服务器端调用connect并且连接成功后,客户端生成connfd描述符,开始传数据.
白话:两边找出可用的端口,形成描述符.客户端发起连接尝试,服务器端接受后连接产生.但还要考虑到阻塞,例如服务器端最大处理1024个连接,超过这个负荷,连接不会被接受而处于等待(阻塞)状态.若等待超时则连接取消等等状况.
====================================内容分割线↑============================
echo服务器端代码
本书原话:在打开监听描述符后,他进入一个无限循环.每次循环都等待一个来自客户端的连接请求,输出已连接客户端的域名和IP地址,并调用echo函数为这些客户端服务. 在echo程序返回后,主程序关闭已连接描述符.一旦客户端和服务器关闭了他们各自的描述符,连接也就终止了.---黑体字是原话
---解读:输出已连接客户端的域名和IP地址不是必须的,是程序这样写的.
代码解读
大致和客户端也差不了多少.讲注意的几点
1>struct sockaddr_storage
本书原话:注意,我们将clientaddr声明为 struct sockaddr_storage类型,而不是struct sockaddr_in类型.根据定义, struct sockaddr_storage结构足够大能够装下任何类型的套接字地址,以保持代码的协议相关性.
---解读:根据定义可以这么做,定义是什么?也不知道.只知道按书上所说这样可行,所以还是老办法"抄".
本书P664代码第21行调用的包装函数Getnameinfo中使用了强制转换,使用(SA *)将 struct sockaddr_storage类型转换成struct sockaddr_in类型,以符合函数对参数类型的要求.
2>迭代服务器
本书原话:简单的echo服务器一次只能处理一个客户端.这种类型的服务器一次一个地在客户端间迭代,称为迭代服务器(iterative server).在第12章中,我们将学习如何建立更加复杂的并发服务器(concurrent server),他能够同时处理多个客户端.
---解读:按照书中的意思,和代码第20行,一个客户端的端口对应一个服务器端口,一个clientfd描述符对应一个connfd描述符.当客户端发来EOF终止信号并关闭client描述符,服务器端同时关闭connfd描述符,连接结束.他们是一一对应的关系.
3>echo函数
本书P664有echo函数的定义,和客户端差不多,用了Rio_readinitb函数初始化缓冲区,Rio_readlineb读取客户端发来的信息,Rio_writen向客户端发送信息.
4>EOF
EOF是一种内核判定的条件,列举了几种情形:
1)磁盘文件,当前文件位置超出文件长度时,发生EOF
每个文件的末尾都有EOF
2)因特网连接,当一个进程关闭连接他的那一端时,发生EOF.
应用:根据向导图,客户端先用Ctrl+D从标准输入流stdin中跳出来,然后调用Close函数,进程关闭,发生EOF,这个EOF似乎没有什么表现.
3)连接另一端的进程在试图读取流中最后一个字节之后的字节时,会检测到EOF
应用:接第2)条
服务器端的echo函数第10行,读取客户端发来的数据,到最后一个字节后,检测到EOF,跳出while,循环结束,接下来执行服务器端主函数中的Close(connfd),此时两边的描述符关闭,连接结束.
while(n=Rio_readlineb(&rio,buf,MAXLINE)!=0)
EOF和标准类似,由程序员知道什么时候出现,怎么用.但EOF是由底层实现(硬件或者内核),不必关心(当然能知道怎么实现更好)
小结
一个网络通信的例子,从建立连接到传送文本.