2017-2018-1 20155302 第十四周作业
重新精学第十一章网络编程相关知识
- 第十一章网络编程因为之前在刘念老师的课上有所涉及有所讲解所以娄老师并没有着重讲这块知识,但我个人认为此章知识非常重要,是我们学习WEB编程和信息安全程序设计的基础,而且这章知识自问之前学习的并不好,并不牢靠,于是借着此次机会更加深入的学习重温一下网络编程方面的知识,好为今后的网络编程道路做好铺垫,学习一章内容两门课都能受益,何乐而不为呢?
首先回答几个问题,这些问题也是之前概念混淆所遗留下来的,此次重新学习解决:
1.网络编程是什么?
- 网络编程的本质是两个设备之间的数据交换,当然,在计算机网络中,设备主要指计算机。数据传递本身没有多大的难度,不就是把一个设备中的数据发送给两外一个设备,然后接受另外一个设备反馈的数据。
- 现在的网络编程基本上都是基于请求/响应方式的,也就是一个设备发送请求数据给另外一个,然后接收另一个设备的反馈。
- 在网络编程中,发起连接程序,也就是发送第一次请求的程序,被称作客户端(Client),等待其他程序连接的程序被称作服务器(Server)。客户端程序可以在需要的时候启动,而服务器为了能够时刻相应连接,则需要一直启动。例如以打电话为例,首先拨号的人类似于客户端,接听电话的人必须保持电话畅通类似于服务器。
- 连接一旦建立以后,就客户端和服务器端就可以进行数据传递了,而且两者的身份是等价的。
- 在一些程序中,程序既有客户端功能也有服务器端功能,最常见的软件就是BT、emule这类软件了。
2.之前课程提到网络编程就是使用socket,于是乎很久以来都有一种错觉网络编程就是socket,或者说我们就会socket,那网络编程必须使用socket吗?除了socket我们还能用些什么呢?
- 常见的socket编程可以理解为操作系统向程序员提供的TCP/IP协议接口。最近了解了NVMe和RDMA,发现TCP/IP成为了这种高速存储场景下的瓶颈。这样场景下,都恨不得直接到硬件。所以,我们可以自己定义并实现一套协议,适用性不谈,底层能力和对网络知识的掌握肯定提升一大截。
- tcp 协议是一个权衡了各种网络资源、主机资源、可靠性、稳定性等等因素的结果,socket 只是 tcp 实现所提供的 API。我们也可以通过 socket 使用 udp。所以,不使用 tcp 而使用 udp,那我们的程序就需要实现 tcp 的功能(假设我们需要),如数据包重排序、拥塞控制、流量控制等等。如果我们连 udp 都不想用,那可以自己封 IP 包,做包切分等等,换句话说我们在实现整个网络协议栈里面挺关键的一部分。也就是说越往底层我们要做的事情就越多。
教材内容精学及回顾
11.1客户端-服务器编程模型
客户端-服务器模型中的基本操作是事务,它由四步组成:
客户端向服务器发送一个请求,发起一个事务;
服务器收到请求后,解释之,并操作它的资源;
服务器给客户端发送一个响应,例如将请求的文件发送回客户端;
客户端收到响应并处理它,例如Web浏览器在屏幕上显示网页。
认识到客户端和服务器是进程而不是具体的机器或主机是重要的。
本节扩展学习
Q:根据事务初步编写客户端及服务器的伪代码
A:
服务器:
循环:
获取缓冲区内容(本地);
发送消息;
接收消息;
打印出来(本地);
客户端:
循环:
接收消息;
打印消息(本地);
获取缓冲区内容(本地);
发送出去;
11.2 网络
网络主机的硬件组成:
以太网段:
桥接以太网:
局域网的概念视图:
通过路由器连接起来多个不兼容的局域网:
互联网络的的至关重要的特性是:它能够采取完全不同的和不兼容技术的各种局域网和广域网组成。每台主机和其他每台主机都是物理相连的。
解决让某台源主机跨过所有不兼容的网络发送数据到另一台目的主机的解决办法是一层运行在每台主机和路由器上的协议软件,这个软件实现一种协议,这种协议必须提供两种基本能力:
1,命名方法:internet协议通过定义一种一直的主机地址格式,消除了这些差异。每台主机会被分配至少一种这种internet地址,这个地址唯一的标识了它。
2,传送机制:互连网络协议定义一种把数据位捆扎成不连续的组块chunk--也就是包---的统一方式,消除了这种差异。一个包由包头和有效载荷组成。
本节扩展学习
Q1:学习了本节虽然学到了很多网络知识但还是脑海中冒出了几个疑问,比如交换机如何知道将帧转发到哪个端口?
A1:使用MAC地址表就可以知道。交换机之所以能够直接对目的节点发送数据包,而不是像集线器一样以广播方式对所有节点发送数据包,最关键的技术就是交换机可以识别连在网络上的节点的网卡MAC地址,并把它们放到一个叫做MAC地址表的地方。这个MAC地址表存放于交换机的缓存中,并记住这些地址,这样一来当需要向目的地址发送数据时,交换机就可在MAC地址表中查找这个MAC地址的节点位置,然后直接向这个位置的节点发送。
Q2:我们平时使用网络的时候经常会遇到丢包现象,那么丢包问题是什么呢?
A2:什么是丢包:数据包的传输,不可能百分之百的能够完成,因为种种原因,总会有一定的损失。碰到这种情况,INTERNET会自动的让双方的电脑根据协议来补包和重传该包。如果网络线路好、速度快,包的损失会非常小,补包和重传的工作也相对较易完成,因此可以近似的将所传输的数据看做是无损的。但是,如果网络线路较差,数据的损失量就会非常大,补包工作又不是百分之百完成的。这种情况下,数据的传输就会出现空洞,造成丢包。
11.3全球IP因特网
因特网应用程序的软硬件组织:
从程序员的角度,可以把因特网看做一个世界范围的主机集合,它满足以下特性:
- 主机集合被映射成一组32位的IP地址。
- 这组IP地址被映射成一组叫做因特网域名的标识符。
- 因特网主机上的进程能够通过连接和任何其他因特网主机上的进程通信。
IP地址结构:
/* IP address structure */
struct in_addr {
uint32_t s_addr; /* Address in network byte order (big-endian) */
};
网络和主机字节顺序间实现转换的函数:
应用程序使用inet_pton和inet_ntop函数来实现IP地址和点分十进制之间的转换:
因特网域名层次结构的一部分:
一个连接是由它两端的套接字地址唯一确定的,这对套接字地址叫做套接字对(socket pair)。由下列代码来表示:(cliaddr:cliport,servaddr:servport)
当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口。然而,服务器套接字地址中的端口通常是某个知名的端口,是和这个服务相对应的。例如,Web服务器通常使用端口80,Email服务器通常使用端口25。
本节扩展学习
Q:编写一个程序,将它的十六进制转换为点分十进制串并打印出来。(把点分十进制转换为十六进制呢?)
A:实验代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*将十六进制参数转换成点分十进制
例如:0x8002c2f2 ->128.2.194.242
*/
int my_htonl(char *argv)
{
struct in_addr inaddr;//网络字节序
unsigned int addr;//点分十进制
sscanf(argv, "%x", &addr);
inaddr.s_addr = htonl(addr);
printf("%s/n", inet_ntoa(inaddr));
return 0;
}
/*将点分十进制参数转换成十六进制
例如: 128.2.194.242->0x8002c2f2
*/
int my_ntohl(char * argv)
{
struct in_addr inaddr;//网络字节序
unsigned int addr;//点分十进制
if(inet_aton(argv, &inaddr) != 0){
addr = ntohl(inaddr.s_addr);
printf("0x%x/n", addr);
}
return 0;
}
int main( )
{
char * test_arry1 = "0x8002c2f2";
char * test_arry2 = "128.2.194.242";
my_htonl(test_arry1 );
my_ntohl(test_arry2);
return 0;
}
书中标准答案:
11.4套接字接口
套接字接口:
套接字编程函数详细学习及理解:
- socket() 函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);
family:协议族
type:套接字类型
protocol:协议类型
SOCKET socket( int af, // 指定地址族 AF_INET int type, // 指定套接字类型(SOCK_STREAM|SOCK_DGRAM) int protocol// 指定协议(TCP/IP | UDP)或(0)时自动选择 );
- connect() 函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符,由socket()函数返回的。
addr:指向对端套接字地址结构的指针。
addrlen:对端套接字地址结构的大小
int connect( SOCKET s, const struct sockaddr FAR *name,//设置对方服务器信息(IP和端口) int namelen //将要连接服务器端口的信息长度 )
- bind() 函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
sockfd: 套接字描述符,由前面的socket()函数成功时返回
myaddr: 指向本地协议地址结构对象的指针
addrlen: 本地协议地址结构的大小。
int bind( SOCKET s, // 结构体对象其名称填写在S处 const struct sockaddr FAR *name, // 指定该套接字的地址指针。 int namelen // 指定该 名字符串的长度 );
- listen函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog);
int listen( SOCKET s, //套接字 int backlog );//设置监听等待队列的最大数目
listen()函数仅有TCP服务器调用,主要有两个作用:
- 将一个未连接的套接字转换为被动套接字,指示内核应该接收指向该套接字的连接请求。(socket()函数创建套接字时,默认设为主动套接字)
- 指定内核应该为相应的套接字排队的最大连接个数。
- accept() 函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 内核创建的新的套接字描述符,用于描述与返回的客户端之间的连接
addr:对端进程(客户端)的协议地址
addrlen: 对端协议地址结构的大小。
getaddrinfo函数详解:
用处:getsockaddr这个函数的功能是将主机名映射成主机的地址,是个新的接口,以取代以前的gethostbyname这个函数,因为后者不能处理ipv6的地址,并且以被标记为废弃。
说明:包含头文件
#include<netdb.h>
函数原型:
int getaddrinfo( const char hostname, const char service, const struct addrinfo *hints, struct addrinfo **result );
参数说明:
hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)
service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等
hints:可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。
result:本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。
返回值:0——成功,非0——出错。
getnameinfo函数详解:
用处:以一个套接口地址为参数,返回一个描述主机的字符串和一个描述服务的字符串。
说明:
函数原型:
int getnameinfo (const struct sockaddr sockaddr, socklen_t addrlen, char host, socklen_t hostlen, char *serv, socklen_t servlen, int flags) ;
成功为0,出错为非0。
参数说明:
NI_MAXHOST:返回的主机字符串的最大长度
NI_MAXSERV:返回的服务字符串的最大长度
本节扩展学习
Q:自主设计一个socket套接字聊天程序并显示字节数。
A:
伪代码:
server.c:
do{
gets(sendBuf);
send();
recv();
puts(recvBuf);
}while(返回值不为-1);
client.c:
do{
recv();
puts(recvBuf);
gets(sendBuf);
send();
}while(返回值不为-1);
实验代码:
server.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFLEN 10
int main(int argc, char **argv)
{
int sockfd, newfd;
struct sockaddr_in s_addr, c_addr;
char buf[BUFLEN];
socklen_t len;
unsigned int port, listnum;
/*建立socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}else
printf("socket create success!\n");
/*设置服务器端口*/
if(argv[2])
port = atoi(argv[2]);
else
port = 4567;
/*设置侦听队列长度*/
if(argv[3])
listnum = atoi(argv[3]);
else
listnum = 3;
/*设置服务器ip*/
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(port);
if(argv[1])
s_addr.sin_addr.s_addr = inet_addr(argv[1]);
else
s_addr.sin_addr.s_addr = INADDR_ANY;
/*把地址和端口帮定到套接字上*/
if((bind(sockfd, (struct sockaddr*) &s_addr,sizeof(struct sockaddr))) == -1){
perror("bind");
exit(errno);
}else
printf("bind success!\n");
/*侦听本地端口*/
if(listen(sockfd,listnum) == -1){
perror("listen");
exit(errno);
}else
printf("the server is listening!\n");
while(1){
printf("*****************聊天开始***************\n");
len = sizeof(struct sockaddr);
if((newfd = accept(sockfd,(struct sockaddr*) &c_addr, &len)) == -1){
perror("accept");
exit(errno);
}else
printf("正在与您聊天的客户端是:%s: %d\n",inet_ntoa(c_addr.sin_addr),ntohs(c_addr.sin_port));
while(1){
_retry:
/******发送消息*******/
bzero(buf,BUFLEN);
printf("请输入发送给对方的消息:");
/*fgets函数:从流中读取BUFLEN-1个字符*/
fgets(buf,BUFLEN,stdin);
/*打印发送的消息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4)){
printf("server 请求终止聊天!\n");
break;
}
/*如果输入的字符串只有"\n",即回车,那么请重新输入*/
if(!strncmp(buf,"\n",1)){
printf("输入的字符只有回车,这个是不正确的!!!\n");
goto _retry;
}
/*如果buf中含有‘\n‘,那么要用strlen(buf)-1,去掉‘\n‘*/
if(strchr(buf,‘\n‘))
len = send(newfd,buf,strlen(buf)-1,0);
/*如果buf中没有‘\n‘,则用buf的真正长度strlen(buf)*/
else
len = send(newfd,buf,strlen(buf),0);
if(len > 0)
printf("消息发送成功,本次共发送的字节数是:%d\n",len);
else{
printf("消息发送失败!\n");
break;
}
/******接收消息*******/
bzero(buf,BUFLEN);
len = recv(newfd,buf,BUFLEN,0);
if(len > 0)
printf("客户端发来的信息是:%s,共有字节数是: %d\n",buf,len);
else{
if(len < 0 )
printf("接受消息失败!\n");
else
printf("客户端退出了,聊天终止!\n");
break;
}
}
/*关闭聊天的套接字*/
close(newfd);
/*是否退出服务器*/
printf("服务器是否退出程序:y->是;n->否? ");
bzero(buf, BUFLEN);
fgets(buf,BUFLEN, stdin);
if(!strncasecmp(buf,"y",1)){
printf("server 退出!\n");
break;
}
}
/*关闭服务器的套接字*/
close(sockfd);
return 0;
}
client.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#define BUFLEN 10
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in s_addr;
socklen_t len;
unsigned int port;
char buf[BUFLEN];
/*建立socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}else
printf("socket create success!\n");
/*设置服务器端口*/
if(argv[2])
port = atoi(argv[2]);
else
port = 4567;
/*设置服务器ip*/
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(port);
if (inet_aton(argv[1], (struct in_addr *)&s_addr.sin_addr.s_addr) == 0) {
perror(argv[1]);
exit(errno);
}
/*开始连接服务器*/
if(connect(sockfd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(errno);
}else
printf("conncet success!\n");
while(1){
/******接收消息*******/
bzero(buf,BUFLEN);
len = recv(sockfd,buf,BUFLEN,0);
if(len > 0)
printf("服务器发来的消息是:%s,共有字节数是: %d\n",buf,len);
else{
if(len < 0 )
printf("接受消息失败!\n");
else
printf("服务器退出了,聊天终止!\n");
break;
}
_retry:
/******发送消息*******/
bzero(buf,BUFLEN);
printf("请输入发送给对方的消息:");
/*fgets函数:从流中读取BUFLEN-1个字符*/
fgets(buf,BUFLEN,stdin);
/*打印发送的消息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4)){
printf("client 请求终止聊天!\n");
break;
}
/*如果输入的字符串只有"\n",即回车,那么请重新输入*/
if(!strncmp(buf,"\n",1)){
printf("输入的字符只有回车,这个是不正确的!!!\n");
goto _retry;
}
/*如果buf中含有‘\n‘,那么要用strlen(buf)-1,去掉‘\n‘*/
if(strchr(buf,‘\n‘))
len = send(sockfd,buf,strlen(buf)-1,0);
/*如果buf中没有‘\n‘,则用buf的真正长度strlen(buf)*/
else
len = send(sockfd,buf,strlen(buf),0);
if(len > 0)
printf("消息发送成功,本次共发送的字节数是:%d\n",len);
else{
printf("消息发送失败!\n");
break;
}
}
/*关闭连接*/
close(sockfd);
return 0;
}
测试截图:
11.5/11.6Web服务器
HTTP请求支持许多不同的方法,包括GET/POST/OPTIONS/HEAD/PUT/DELETE/TRACE。其中GET最常用。
GET方法指导服务器生成和返回URI(统一资源标识符,是URL的后缀,包括文件名和可选的参数)。
请求报头为服务器提供了额外的信息,例如浏览器的商标名等。
HTTP响应和请求是类似的,它包括:一个响应行,后面跟随0个或多个响应报头,然后是终止报头的空行,再跟随一个响应主体。
本节扩展学习
Q1:家庭作业11.9
A1:
serve_static中的存储器映射语句改为:
srcfd = open(filename, O_RDONLY, 0); srcp = (char*)malloc(sizeof(char)*filesize); rio_readn(srcfd, srcp, filesize); close(srcfd); rio_writen(fd, srcp, filesize); free(srcp);
Q2:家庭作业11.8
A2:
在main函数之前加入代码:
int chdEnded ;
#include <signal.h>
void child_signal(int sig)
{
pid_t pid;
while((pid = waitpid(-1, NULL, WNOHANG)) > 0)
;
chdEnded = 1;
}
在main函数中添加语句 signal(SIGCHILD, child_handle);
每次accept之前,让chdEnded = 0;
并且在doit()中的serve_dynamic
之后添加:
while(!chdEnded) pause();//or do sth
删掉serve_dynamic里的wait(NULL);
结对学习成果
我和我的结对成员秦诗茂认真的重新温习了本章,就套接字编程函数中的参数以及所用到的函数含义做了深入的探讨学习,并且改进了socket聊天程序,从原本只能对话升级成了可以统计字节数,秦同学还给我讲了如何传送文件的代码改进方式,我虚心向他学习了。并且找了些网络编程相关的题目一起做。
结对照片: