2017-2018-1 20155333 《信息安全系统设计基础》第八周学习总结
2017-2018-1 20155333 《信息安全系统设计基础》第八周学习总结
教材学习内容总结
网络编程
1、 每个网络应用都是基于客户端-服务器模型的。客户端-服务器模型中的基本操作是事务(transaction)(这里的事务与数据库中的事务有区别,没有数据库事务的特性,如原子性,这时原事务仅仅是客户端和服务器之间执行的一系列步骤)。认识到客户端和服务器是进程,而不是在本上下文中常被称为的机器或者主机。
- 事务由四步组成:
(1)客户端向服务器发送一个请求,发起一个事务;
(2)服务器收到请求后,解释之,并操作它的资源;
(3)服务器给客户端发送一个响应,例如将请求的文件发送回客户端;
(4)客户端收到响应并处理它,例如Web浏览器在屏幕上显示网页。
2、网络
一个以太网段,包括电缆和集线器;每根电缆都有相同的最大位带宽;集线器不加分辩地将一个端口上收到的每个位复制到其他所有的端口上。因此,每台主机都能看到每个位。
使用电缆和网桥,多个以太网段可以连接成较大的局域网,称为桥接以太网。这些电缆的带宽可以是不同的。
多个不兼容的局域网可以通过叫做路由器的特殊计算机连接起来,组成一个internet互联网络。
3、数据传输
4、因为因特网主机可以有不同的主机字节顺序,TCP/IP为任意整数数据项定义了一致的网络字节顺序,大端字节顺序。
5、因特网的连接
套接字(socket)是连接的端点(end-point)。每个套接字都有相应的套接字地址,由一个因特网地址和一个16位的整数端口组成的,用“地址:端口”来表示。
一个连接是由它两端的套接字地址惟一确定的。这对套接字地址叫做套接字对:(cliaddr:cliport, servaddr:servport)
6、套接字接口
套接字接口(socket interface)是一组用来结合unit I/O函数创建网络应用的函数。
从unit内核的角度来看,套接字就是通信的端点;从unix程序的角度来看,套接字就是一个有相应描述符的打开文件。
7、EOF是由内核检测到的一个条件,超出文件末端。
8、socket 函数
客户端和服务器使用socket函数来创建一个套接字描述符。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int tpye,int protocol);
clientfd=socket(AF_INET,SOCK_STREAM,0);
其中AF_INET表示我们正在使用因特网,而SOCK_STREAM表示这个套接字是因特网连接的一个端点。
9、connect函数
客户端通过调用connect函数来建立与服务器的连接。
#include <sys/socket.h>
int connect(int sockfd,struct sockadd *serv_addr,int addrlen);
connect函数试图与套接字地址位serv_addr的服务器建立连接,它被阻塞直到连接成功或发生错误。
10、bind函数
下面的bind、listen和accept函数被服务器用来与客户端建立连接。
#include <sys/socket.h>
int bind(int sockfd,struct sockaddr *my_addr,int addrlen);
bind函数告诉内核将套接字地址和套接字描述符联系起来。
11、listen函数
服务器是被动接收客户端连接请求的,listen函数将套接字描述符从主动套接字转化为监听套接字。
12、accept函数
服务器通过调用accept函数来等待来自客户端的连接请求。
#include <sys/socket.h>
int accept(int listenfd,struct sockaddr *addr,int *addrlen);
accpet函数等待来自客户端的连接请求到达监听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个连接描述符connfd。
13、监听描述符和连接描述符的区别
监听描述符是供客户端连接请求的使用一个端点,它被创建一次,并存在于服务器的整个生命周期。
连接描述符是客户端和服务器之间已成功连接的一个端点,服务器每次接受连接请求时都会创建一次,它只存在于服务器每次为一个客户端服务的过程中。
区分监听描述符和连接描述符是有必要的,因为这样使得我们可以建立并发服务器,它能够同时处理许多客户端连接。
并发编程
1、基于预线程化(prethreading)的并发服务器
常规的并发服务器中,我们为每一个客户端创建一个新线程,代价较大。一个基于预线程化的服务器通过使用“生产者-消费者模型”来试图降低这种开销。
服务器由一个主线程和一组worker线程组成的,主线程不断地接受来自客户端的连接请求,并将得到的连接描述符放在一个共享的缓冲区中。每一个worker线程反复从共享缓冲区中取出描述符,为客户端服务,然后等待下一个描述符。
- 示例代码
#include "csapp.h"
#include "sbuf.h"
#define NTHREADS 4
#define SBUFSIZE 16
void echo_cnt(int connfd);
void *thread(void *vargp);
sbuf_t sbuf; /* shared buffer of connected descriptors */
int main(int argc, char **argv)
{
int i, listenfd, connfd, port, clientlen=sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
pthread_t tid;
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
port = atoi(argv[1]);
sbuf_init(&sbuf, SBUFSIZE);
listenfd = Open_listenfd(port);
for (i = 0; i < NTHREADS; i++) /* Create worker threads */
Pthread_create(&tid, NULL, thread, NULL);
while (1) {
connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
sbuf_insert(&sbuf, connfd); /* Insert connfd in buffer */
}
}
void *thread(void *vargp)
{
Pthread_detach(pthread_self());
while (1) {
int connfd = sbuf_remove(&sbuf); /* Remove connfd from buffer */
echo_cnt(connfd); /* Service client */
Close(connfd);
}
}
如上模型组成事件驱动服务器,事件驱动程序创建它们自己的并发逻辑流,这些逻辑流被模型化为状态机,带有主线程和worker线程的简单状态机。
2、其他并发问题
一个函数被称为线程安全(thread-safe)的,当且仅当多个线程反复地调用时,它会一下产生正确的结果。
下面是四类不安全(相交)的函数:
(1)不保护共享变量的函数
利用P,V操作解决这个问题。
(2)保持跨越多个调用的状态的函数
- 示例代码1
#include <stdio.h>
/* $begin rand */
unsigned int next = 1;
/* rand - return pseudo-random integer on 0..32767 */
int rand(void)
{
next = next*1103515245 + 12345;
return (unsigned int)(next/65536) % 32768;
}
/* srand - set seed for rand() */
void srand(unsigned int seed)
{
next = seed;
}
/* $end rand */
int main()
{
srand(100);
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
exit(0);
}
srand设置种子,调用rand生成随机数。多线程调用时就出问题了。我们可以重写之解决,使之不再使用任何静态数据,取而代之地依靠调用者在参数中传递状态信息。
- 示例代码2
#include <stdio.h>
/* $begin rand_r */
/* rand_r - a reentrant pseudo-random integer on 0..32767 */
int rand_r(unsigned int *nextp)
{
*nextp = *nextp * 1103515245 + 12345;
return (unsigned int)(*nextp / 65536) % 32768;
}
/* $end rand_r */
int main()
{
unsigned int next = 1;
printf("%d\n", rand_r(&next));
printf("%d\n", rand_r(&next));
printf("%d\n", rand_r(&next));
exit(0);
}
(3)返回指向静态变量的指针的函数
某些函数(如gethostbyname)将结果放在静态结构中,并返回一个指向这个结构的指针。多线程并发可能引发灾难,因为正在被一个线程使用的结果会被另一个线程悄悄覆盖。
两种方法处理:
一是重写之。使得调用者传递存放结果的结构的地址,这就消除了共享数据。
第二种方法是:使用称为lock-and-copy的技术。在每一个调用位置,对互斥锁加锁,调用线程不安全函数,动态地为结果分配存储器,copy函数返回结果到这个存储器位置,对互斥锁解锁。
- 示例代码3
#include "csapp.h"
static sem_t mutex; /* protects calls to gethostbyname */
static void init_gethostbyname_ts(void)
{
Sem_init(&mutex, 0, 1);
}
/* $begin gethostbyname_ts */
struct hostent *gethostbyname_ts(char *hostname)
{
struct hostent *sharedp, *unsharedp;
unsharedp = Malloc(sizeof(struct hostent));
P(&mutex);
sharedp = gethostbyname(hostname);
*unsharedp = *sharedp; /* copy shared struct to private struct */
V(&mutex);
return unsharedp;
}
/* $end gethostbyname_ts */
int main(int argc, char **argv)
{
char **pp;
struct in_addr addr;
struct hostent *hostp;
if (argc != 2) {
fprintf(stderr, "usage: %s <hostname>\n", argv[0]);
exit(0);
}
init_gethostbyname_ts();
hostp = gethostbyname_ts(argv[1]);
if (hostp) {
printf("official hostname: %s\n", hostp->h_name);
for (pp = hostp->h_aliases; *pp != NULL; pp++)
printf("alias: %s\n", *pp);
for (pp = hostp->h_addr_list; *pp != NULL; pp++) {
addr.s_addr = *((unsigned int *)*pp);
printf("address: %s\n", inet_ntoa(addr));
}
}
else {
printf("host %s not found\n", argv[1]);
}
exit(0);
}
(4)调用线程不安全函数的函数
f调用g。如果g是(2)类函数,则f也是不安全的,只能得写。如果g是1)或3)类函数,则利用互斥锁保护调用位置和任何想得到的共享数据,f仍是线程安全的。如上例中。
3、可重入性
可重入函数(reenterant function)具有这样的属性:当它们被多个线程调用时,不会引用任何共享数据。
可重入函数通常比不可重入函数高效一些,因为不需要同步操作。
如果所有的函数参数都是传值传递(没有指针),且所有的数据引用都是本地的自动栈变量(没有引用静态或全局变量),则函数是显式可重入的,无论如何调用,都没有问题。
允许显式可重入函数中部分参数用指针传递,则隐式可重入的。在调用线程时小心传递指向非共享数据的指针,它才是可重入。如rand_r。
可重入性同时是调用者和被调用者的属性。
4、竞争
当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点时,就会发生竞争(race)。
- 示例代码1
#include "csapp.h"
#define N 4
void *thread(void *vargp);
int main()
{
pthread_t tid[N];
int i;
for (i = 0; i < N; i++)
Pthread_create(&tid[i], NULL, thread, &i);
for (i = 0; i < N; i++)
Pthread_join(tid[i], NULL);
exit(0);
}
/* thread routine */
void *thread(void *vargp)
{
int myid = *((int *)vargp);
printf("Hello from thread %d\n", myid);
return NULL;
}
- 示例代码2(消除竞争)
#include "csapp.h"
#define N 4
void *thread(void *vargp);
int main()
{
pthread_t tid[N];
int i, *ptr;
for (i = 0; i < N; i++) {
ptr = Malloc(sizeof(int));
*ptr = i;
Pthread_create(&tid[i], NULL, thread, ptr);
}
for (i = 0; i < N; i++)
Pthread_join(tid[i], NULL);
exit(0);
}
/* thread routine */
void *thread(void *vargp)
{
int myid = *((int *)vargp);
Free(vargp);
printf("Hello from thread %d\n", myid);
return NULL;
}
5、死锁
信号量引入一个潜在的运行是错误-死锁。死锁是因为每个线程都在等待其他线程运行一个根本不可能发生的V操作。
避免死锁是很困难的。当使用二进制信号量来实现互斥时,可以用如下规则避免:
如果用于程序中每对互斥锁(s,t),每个既包含s也包含t的线程都按照相同顺序同时对它们加锁,则程序是无死锁的。
教材学习中的问题和解决过程
- 问题1:
- 问题1解决方案:
- 问题2:
- 问题2解决方案:
- ...
代码调试中的问题和解决过程
- 问题1:XXXXXX
- 问题1解决方案:XXXXXX
- 问题2:XXXXXX
- 问题2解决方案:XXXXXX
- ...
代码托管
上周考试错题总结
- 错题1及原因,理解情况
- 错题2及原因,理解情况
- ...
结对及互评
点评模板:
- 博客中值得学习的或问题:
- xxx
- xxx
- ...
- 代码中值得学习的或问题:
- xxx
- xxx
- ...
- 其他
本周结对学习情况
- [2015532](http://www.cnblogs.com/zjy1997/)
- 结对照片
- 结对学习内容
- 教材第四章
- XXXX
- ...
其他(感悟、思考等,可选)
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 10/10 | 1/1 | 10/10 | |
第二周 | 80/90 | 1/2 | 15/25 | |
第三周 | 100/190 | 1/3 | 15/40 | |
第四周 | 150/340 | 1/4 | 18/58 | |
第五周 | 2/6 | 20/78 | ||
第六周 | 2/8 | 20/98 | ||
第七周 | 2124/ | 2/10 | 20/118 | |
第八周 | 2/12 | 30/148 | ||
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。 | ||||
耗时估计的公式 | ||||
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。 |
-
计划学习时间:35小时
-
实际学习时间:30小时
-
改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)