2017-2018-1 20155321 《信息安全系统设计基础》第十四周学习总结
教材学习内容总结
- 通过这学期对教材的学习以及这几次实验的练习。我认为自己对于第11章——网络编程的学习比较不扎实,因此这周主要学习了这一章。
基本概念
-
协议
- 计算机网络中实现通信有一些约定,例如速率、传输代码、代码结构等,这些约定即为通信协议
- 两节点间要成功通信必须使用共同的“语言”,这些被通信各方共同遵守的约定、语言、规则被称为协议
- 在因特网中,最为通用的网络协议是TCP/IP协议
- 个人学习的感受是协议主要是为了双方能够正确地解析数据,避免解析数据发生错乱
-
网络分层模型
- 物理层下面即为以太网,传输通过以太网传输到另外一台计算机上。每一层都有不同的作用,每一层用到的协议均不一样,如下图所示:
- 物理层下面即为以太网,传输通过以太网传输到另外一台计算机上。每一层都有不同的作用,每一层用到的协议均不一样,如下图所示:
-
TCP/IP协议族
- 为网际数据通信提供通路
- 主要分为三部分
- IP协议
- TCP和UDP协议
- 处于TCP和UDP上的一组协议,专门开发的应用程序
-
网络层协议
- IP协议:在两个主机间传输数据报,提供的是非连接型传递服务(不维护后续数据报的状态信息,因此不太可靠)
- 主要功能:数据传送、寻址、路由选择、数据报文的分段
- 地址分为5类(A[0-127]、B[128-191]、C[192-233]统一分配,D[224-239]、E[240-255]为特殊地址)
- ICMP协议:报告网络上的某些出错情况,允许路由器传输差错信息或测试报文
- ARP协议:处于网络层和数据链路层之间,主要是为了32位的IP地址和48位的MAC地址之间的翻译
- IP协议:在两个主机间传输数据报,提供的是非连接型传递服务(不维护后续数据报的状态信息,因此不太可靠)
-
传输层协议
- TCP协议
- 可靠的,面向连接的服务
- 功能:监听输入对话建立请求、请求另一网络站点对话、可靠的发送和接收数据、关闭对话
- UDP协议
- 不可靠的,非连接的服务
- 不必在传送数据时建立对话,不适用差错检查,开销比较小
- TCP协议
-
应用层协议
- Telnet:远程登录
- FTP和TFTP:文件传送协议
- SMTP:简单的文件传送协议
- DNS:域名服务
-
数据封装
- 数据报主要由协议的头部(包括差错、源和目的地址等)和协议的体部(真正传输的内容)两部分组成
- 数据报主要由协议的头部(包括差错、源和目的地址等)和协议的体部(真正传输的内容)两部分组成
-
端口号
- TCP/UDP协议使用16位整数存储端口号,所以每个主机有65535个端口
- 可在
/etc/services
下进行查看该文件,如下图所示
TCP编程
-
Socket套接字
- 特殊的I/O,提供对应的文件描述符
- 一个完整的Socket都有一个相关描述(协议、本地地址、本地端口、远程地址、远程端口)
- 每一个Socket有一个本地的唯一Socket,由操作系统分配
- 创建Socket
int socket(int domain,int type,int protocol)
- 创建成功返回内核文件描述表中的socket描述符,出错返回-1;包含头文件<sys/socket.h>
-
字节序
- 分为大端和小端字节序
- 网络协议使用网络字节序即大端字节序
- 字节序转换函数
uint32_t htonl(uint32_t hostlong)
:32位的主机转网络uint16_t htons(uint16_t hostshort)
:16位的主机转网络uint32_t ntohl(uint32_t netlong)
:32位的网络转主机uint16_t ntohs(uint16_t netshort)
:16位的网络转主机
-
地址结构
- 通用地址结构
#include <sys/socket.h> struct sockaddr{ unsigned short sa_family; //协议族 char sa_data[14]; //协议地址 }
- 因特网地址结构
struct in_addr{ in_addr_t s_addr; } struct sockaddr_in{ short int sin_family; unsigned short int sin_port; struct in_addr sin_addr; unsigned char sin_zero[8]; }
- 两者的转换需要强转
-
IPV4地址族和字符地址间的转换
const char *inet_ntop(int domain,const void *restrict addr,char *restrict str,socklen_t size);
:网络字节序转换为点分十进制int inet_pton(int domain,const void *restrict str,void *restrict addr);
:点分十进制转换为网络字节序
-
TCP客户端服务器编程模型
- 客户端调用序列
- 调用socket函数创建套接字
- 调用connect连接服务器端
- 调用I/O函数与服务器通讯
- 调用close关闭套接字
- 服务器端调用序列
- 调用socket函数创建套接字
- 调用bind绑定本地地址和端口
- 调用listen启动监听
- 调用accept从已连接队列中取得客户连接
- 调用I/O函数与客户端通讯
- 调用close关闭套接字
- 总体流程图如下
- 客户端调用序列
-
套接字与地址绑定
- 绑定地址:bind()函数
- 查找绑定到套接字的地址:getsockname()函数
- 获取对方地址:getpeername()函数
-
建立连接
- 服务器端:listen()函数
- 客户端:connect()函数
-
实践1:完成课上某一次实践内容:服务器端给客户端回复当前时间信息
- 服务器端
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <signal.h>
#include <time.h>
int sockfd;
void sig_handler(int signo)
{
if(signo == SIGINT){
printf("server close
");
close(sockfd);
exit(1);
}
}
void out_addr(struct sockaddr_in *clientaddr)
{
int port = ntohs(clientaddr->sin_port);
char ip[16];
memset(ip,0,sizeof(ip));
inet_ntop(AF_INET,&clientaddr->sin_addr.s_addr,ip,sizeof(ip));
printf("client:%s(%d) connected
",ip,port);
}
void do_service(int fd)
{
long t = time(0);
char *s = ctime(&t);
size_t size = strlen(s) * sizeof(char);
if(write(fd,s,size) != size){
printf("error");
}
}
int main(int argc,char *argv[])
{
if(argc < 2){
printf("usage:%s #port
",argv[0]);
exit(1);
}
if(signal(SIGINT,sig_handler) == SIG_ERR){
printf("error!");
exit(1);
}
sockfd = socket(AF_INET ,SOCK_STREAM,0);;
struct sockaddr_in serveraddr;
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
serveraddr.sin_addr.s_addr = INADDR_ANY;
if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr)) < 0){
printf("bind error");
exit(1);
}
if(listen(sockfd,10) < 0){
printf("listen error");
exit(1);
}
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
while(1){
int fd = accept(sockfd,(struct sockaddr*)&clientaddr,&clientaddr_len);
if(fd < 0){
printf("error");
continue;
}
out_addr(&clientaddr);
do_service(fd);
close(fd);
}
return 0;
}
- 客户端
#include <netdb.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <memory.h>
int main(int argc,char *argv[])
{
if(argc < 3){
printf("usage:%s #port
",argv[0]);
exit(1);
}
int sockfd = socket(AF_INET ,SOCK_STREAM,0);
if(sockfd < 0){
printf("error");
exit(1);
}
struct sockaddr_in serveraddr;
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&serveraddr.sin_addr.s_addr);
if(connect(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr)) < 0){
printf("connect error");
exit(1);
}
char buffer[1024];
memset(buffer,0,sizeof(buffer));
size_t size;
if((size = read(sockfd,buffer,sizeof(buffer))) < 0){
printf("read error");
}
if(write(STDOUT_FILENO,buffer,size) != size){
printf("write error");
}
close(sockfd);
return 0;
}
运行截图如下:
-
自定义协议编程
- 在之前的学习中,都是直接采用了已给好的协议规则,因此想学习以下自己写一个协议给自己用,当然目前自己写的协议十分简单,只是模拟这个过程,在自己写的协议的头部只有校验码这个功能
- 实践2:自己定义协议
- 先编写头文件
#ifndef _MSG_H_ #define _MSG_H_ #include <sys/types.h> typedef struct{ char head[10]; char checknum; char buff[512]; }Msg; extern int write_msg(int sockfd,char *buff,size_t len); extern int read_msg(int sockfd,char *buff,size_t len); #endif
-
再编写服务器端和客户端会使用到的功能函数
#include "msg.h" #include <unistd.h> #include <string.h> #include <memory.h> #include <sys/types.h> static unsigned char msg_check(Msg *message) { unsigned char s=0; int i; for(i=0;i < sizeof(message->head);i++){ s += message->head[i]; } for(i=0;i < sizeof(message->buff);i++){ s += message->buff[i]; } return s; } int write_msg(int sockfd,char *buff,size_t len) { Msg message; memset(&message,0,sizeof(message)); strcpy(message.head,"20155321"); memcpy(message.buff,buff,len); message.checknum = msg_check(&message); if(write(sockfd,&message,sizeof(message)) != sizeof(message)){ return -1; } } int read_msg(int sockfd,char *buff,size_t len) { Msg message; memset(&message,0,sizeof(message)); size_t size; if((size = read(sockfd,&message,sizeof(message))) < 0){ return -1; } else if(size == 0){ return 0; } unsigned char s = msg_check(&message); if((s == (unsigned char)message.checknum) && (!strcmp("20155321",message.head))){ memcpy(buff,message.buff,len); return sizeof(message); } return -1; }
-
在目前实践均是一个客户端和一个服务器进行通信,没有实现并发,但在正常的应用中,应该是一个服务器端和多个客户端进行通信
-
实践3:服务器端的并发处理
- 多进程实现
- 思路:父进程通过fork()函数创建子进程与每个客户端进行通信,父进程自己循环执行accept()语句即可,
- 服务器端
- 服务器端在accept语句后创建子进程即可
pid_t pid = fork(); if(pid < 0){ continue; }else if(pid == 0){ //child out_addr(&clientaddr); do_service(fd); close(fd); break; }else{ //parent close(fd); }
-
客户端
- 客户端只需要在实践1的基础上修改读写操作即可
char buff[512]; size_t size; char *promt = ">"; while(1){ memset(buff,0,sizeof(buff)); write(STDOUT_FILENO,prompt,1); size = read(STDIN_FILENO,buff,sizeof(buff)); if(size < 0) continue; buff[size-a]=' '; if(write_msg(sockfd,buff,sizeof(buff)) < 0){ printf("error"); continue; }else{ if(read_msg(sockfd,buff,sizeof(buff)) < 0){ printf("error"); continue; }else{ printf("%s ",buff); } } }
-
运行结果如下:
-
实践4:服务器端的并发处理
- 多线程实现
- 因为如果有大量的客户端访问服务器端,服务器端都要开启一个进程的话就会导致服务器端的效率很低下,因此应该采用多线程进行处理
- 服务器端
- 只需在实践3的基础上把进程的相关部分改为线程即可,主要修改如下部分:
线程处理函数部分: void out_fd(int fd) { struct sockaddr_in addr; socklen_t len=sizeof(addr); if(getpeername(fd,(struct sockaddr*)&addr,&len) < 0){ printf("error"); return; } char ip[16]; memset(ip,0,sizeof(ip)); int port = ntohs(addr.sin_port); inet_ntop(AF_INET,&addr.sin_addr.s_addr,ip,sizeof(ip)); printf("%16s(%5d) closed! ",ip,port); } void* th_fn(void *arg) { int fd = (int)arg; do_service(fd); out_fd(fd); close(fd); return (void*)0; } 主函数部分: pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); while(1){ int fd = accept(sockfd,NULL,NULL); if(fd < 0){ printf("error"); continue; } pthread_t th; int err; if((err = pthread_create(&th,&attr,th_fn,(void*)fd)) != 0){ printf("error"); } pthread_attr_destroy(&attr); }
- 客户端
- 客户端则直接使用实践3的代码即可
- 运行结果如下
UDP编程
-
特点
- 无连接、不安全的,适用于可靠性不需要太高的情况
- 效率比TCP协议要高,适用于视频、音频等
-
所涉及到的函数
- 发送数据:
ssize_t sendto(int sockfd,const void *buf,size_t nbytes,int flag,const struct sockaddr *desaddr,socklen_t destlen);
- 结束数据:
ssize_t recvfrom(int sockfd,void *restrict buf,size_t len,int flag,struct sockaddr *restrict addr,socklen_t *restrict adddrlen);
- 发送数据:
-
实践5:用UDP协议重新写实践1
- 功能:服务器端返回客户端当前时间信息
- 服务器端
#include <netdb.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <memory.h> #include <signal.h> #include <time.h> int sockfd; void sig_handler(int signo) { if(signo == SIGINT){ printf("server close "); close(sockfd); exit(1); } } void out_addr(struct sockaddr_in *clientaddr) { int port = ntohs(clientaddr->sin_port); char ip[16]; memset(ip,0,sizeof(ip)); inet_ntop(AF_INET,&clientaddr->sin_addr.s_addr,ip,sizeof(ip)); printf("client:%s(%d) connected ",ip,port); } void do_service() { struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); char buffer[1024]; memset(buffer,0,sizeof(buffer)); if(recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&clientaddr,&len) < 0){ printf("recvfrom error"); }else{ out_addr(&clientaddr); printf("client send into:%s ",buffer); long int t = time(0); char *ptr = ctime(&t); size_t size = strlen(ptr) * sizeof(char); if(sendto(sockfd,ptr,size,0,(struct sockaaddr*)&clientaddr,len) < 0){ printf("sendto error"); } } } int main(int argc,char *argv[]) { if(argc < 2){ printf("usage:%s #port ",argv[0]); exit(1); } if(signal(SIGINT,sig_handler) == SIG_ERR){ printf("error!"); exit(1); } sockfd = socket(AF_INET ,SOCK_DGRAM,0); if(sockfd < 0){ printf("socket error!"); exit(1); } int ret,opt=1; if((ret = setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))) < 0){ printf("error"); exit(1); } struct sockaddr_in serveraddr; memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(atoi(argv[1])); serveraddr.sin_addr.s_addr = INADDR_ANY; if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr)) < 0){ printf("bind error"); exit(1); } while(1){ do_service(); } return 0; }
- 客户端
- 在原来tcp编程的基础上主要修改send函数
#include <netdb.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <memory.h> int main(int argc,char *argv[]) { if(argc < 3){ printf("usage:%s #port ",argv[0]); exit(1); } int sockfd = socket(AF_INET ,SOCK_DGRAM,0); if(sockfd < 0){ printf("socket error!"); exit(1); } struct sockaddr_in serveraddr; memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&serveraddr.sin_addr.s_addr); char buffer[1024] = "hello 20155321lrt"; if( sendto( sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&serveraddr,sizeof(serveraddr) ) < 0 ){ printf("sendto error"); exit(1); }else{ memset(buffer,0,sizeof(buffer)); if(recv(sockfd,buffer,sizeof(buffer),0) < 0){ printf("recvfrom error"); exit(0); }else{ printf("%s",buffer); } } close(sockfd); return 0; }
- 运行结果如下
教材学习中的问题和解决过程
-
问题1:在实践1中,对于服务器端的编程,为什么要首先建立一个监听socket?
-
问题1解决方案:监听socket为了给每一个用户提供一个与其相连接的socket,相当于一个socket的管理者,如下图所示:
-
问题2:在实践3,对于服务器端的编程,如果客户端因为某种原因突然断开连接的话服务器端该如何处理?
-
问题2解决方案:如果客户端突然断开连接,那么服务器端调用的read函数应该无法正常运行,其返回值就为0,因此可以通过测试返回值进行处理。同理,如果服务器端正在给客户端写数据而客户端突然断开连接,那么write函数会将errno置为EPIPE,因此直接测试这个值即可。如下所示:
size_t size;
if((size = read_msg(fd,buff,sizeof(buff))) < 0){
printf("error");
break;
}else if(size == 0){
break;
}else{
printf("%s
",buff);
if(write_msg(fd,buff,sizeof(buff)) < 0){
if(errno == EPIPE){
break;
}
printf("error");
}
}
- 问题3:在udp的协议中,是不需要connect()中,因为只是在tcp协议中,通过调用connect()函数进行三次握手,那如果在udp协议中调用了connect()函数会怎么样呢?是不是也就做到了三次握手呢?
- 问题3解决方案:通过对网上资料的学习,在udp编程中即使调用了connect()函数也并没有经过三次握手,只是在内核中记录了服务器端的地址信息(例如ip和端口号),因此在udp编程中如果调用了connect()函数,那么也就可以不用sendto()函数,而直接使用send函数即可,其实本质上就是没有指定服务器端的地址信息也没有问题,因为在send前内核已经记录了相关信息。
代码调试中的问题和解决过程
-
问题1:在实践1中,当我写好服务器端的程序的时候,我想先检查服务器端的程序的逻辑是否有问题,如果非要写一个客户端后才能进行调试就显得非常麻烦,有没有办法在不写客户端程序后直接简单测试一下服务器端能否正常运行
-
问题1解决方案:可以使用应用层的远程登录帮助测试,输入命令
telnet 127.0.0.1 端口号
进行测试即可(在本机上测试直接用localhost就好,如果不是在本机上测试,则需要修改命令),如下图所示:
-
问题2:在实践3中,因为想直接用到实践2中所写的协议,但在编译服务器端的时候出现了以下问题,说找不到自己写的处理函数,如下图所示:
-
问题2解决方案:在编译的时候,要把自己写的关于协议那部分的处理函数封装成一个模块,即为一个.o文件,然后在命令行下编译的时候,直接加进去即可,如下图所示:
代码托管
上周考试错题总结
- 错题1:( 多选题 | 1 分)有关RAM的说法,正确的是()
A.SRAM和DRAM掉电后均无法保存里面的内容。
B.DRAM将一个bit存在一个双稳态的存储单元中
C.一般来说,SRAM比DRAM快
D.SRAM常用来作高速缓存
E.DRAM将每一个bit存储为对一个电容充电
F.SRAM需要不断刷新
G.DRAM被组织为二维数组而不是线性数组
-
正确答案: A C D E G
-
错题2:Unix/Linux中,对于代码fd=open("foo",O_WRONLY,0766),umask=022,下面说法正确的是()
A.进程对foo是只写的
B.同组成员能写foo
C.使用者可以执行foo
D.任何人都可以写foo
-
正确答案: A C
-
错题3:关于open(2),下面说法正确的是( )
A.flag 参数中O_RDONLY,O_WRONLY,O_RDWR至少要有一个
B.O_RDONLY|O_WRONLY == O_RDWR
C.fd=open("foo.txt",O_WRONLY|O_APPEND,0),调用write(fd,buff,n)写入foo.txt的数据不会破坏已有数据。
D.fd=open("foo.txt",O_WRONLY|O_APPEND,0644),必将导致其他人不能写foo.txt
- 正确答案: A C
其他(感悟、思考等,可选)
经过本周对网络编程的学习,较大的弥补了之前学习上的一些不解和漏洞,第一次接触网络编程的时候,较多的函数调用以及名词概念难以完全记住和消化,通过再次学习,不断地强化记忆,也发现了第一次学习时没有注意到的地方,有时候就是这些地方导致了后续学习上的一些困难,前期的基本概念和理解扎实一些还是对后续学习有很大帮助的。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第十四周 | 623/1352 | 1/20 | 15/210 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
-
计划学习时间:15小时
-
实际学习时间:12小时
-
改进情况:多加练习