2017-2018-1 20155326 《信息安全系统设计基础》第13周学习总结

2017-2018-1 20155326 《信息安全系统设计基础》第13周学习总结

回顾一学期所学知识,我感觉网络编程和并发很重要。

服务器按处理方式可以分为迭代服务器和并发服务器两类。平常用C写的简单Socket客户端服务器通信,服务器每次只能处理一个客户的请求,它实现简单但效率很低,通常这种服务器被称为迭代服务器。 然而在实际应用中,不可能让一个服务器长时间地为一个客户服务,而需要其具有同时处理 多个客户请求的能力,这种同时可以处理多个客户请求的服务器称为并发服务器,其效率很高却实现复杂。在实际应用中,并发服务器应用的最广泛。

这里我先总结了初级的网络编程部分,再深入到并发编程。

TCP/IP协议概述

  • tcp/ip模型4层:

截图1

应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。(包括http超文本传输协议 ftp文件传输协议 telnet远程登录 ssh安全外壳协议 stmp简单邮件发送 pop3收邮件)

传输层:负责端对端之间的通信会话连接和建立。传输协议的选择根据数据传输方式而定。(包括tcp传输控制协议,udp用户数据包协议)

网络层:负责将数据帧封装成IP数据报,并运行必要的路由算法。(包括ip网际互联协议 icmp网络控制消息协议 igmp网络组管理协议)

网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元。(包括arp地址转换协议,rarp反向地址转换协议,mpls多协议标签交换)

  • TCP与UDP性质比较

    TCP协议:传输控制协议 面向连接的协议 能保证传输安全可靠 速度慢(有3次握手)

    UDP协议:用户数据包协议 非面向连接 速度快 不可靠

通常是ip地址后面跟上端口号:ip用来定位主机 port区别应用(进程)

http的端口号80 ssh-->22 telnet-->23 ftp-->21 用户自己定义的通常要大于1024

TCP协议

  • TCP是TCP/IP体系中面向连接的运输层协议,它提供全双工和可靠交付的服务。它采用许多机制来确保端到端结点之间的可靠数据传输,如采用序列号、确认重传、滑动窗口等。

  • 首先,TCP要为所发送的每一个报文段加上序列号,保证每一个报文段能被接收方接收,并只被正确的接收一次。

  • 其次,TCP采用具有重传功能的积极确认技术作为可靠数据流传输服务的基础。这里“确认”是指接收端在正确收到报文段之后向发送端回送一个确认(ACK)信息。发送方将每个已发送的报文段备份在自己的缓冲区里,而且在收到相应的确认之前是不会丢弃所保存的报文段的。“积极”是指发送发在每一个报文段发送完毕的同时启动一个定时器,加入定时器的定时期满而关于报文段的确认信息还没有达到,则发送发认为该报文段已经丢失并主动重发。为了避免由于网络延时引起迟到的确认和重复的确认,TCP规定在确认信息中捎带一个报文段的序号,使接收方能正确的将报文段与确认联系起来。

  • 最后,采用可变长的滑动窗口协议进行流量控制,以防止由于发送端与接收端之间的不匹配而引起的数据丢失。这里所采用的滑动窗口协议与数据链路层的滑动窗口协议在工作原理上完全相同,唯一的区别在于滑动窗口协议用于传输层是为了在端对端节点之间实现流量控制,而用于数据链路层是为了在相邻节点之间实现流量控制。TCP采用可变长的滑动窗口,使得发送端与接收端可根据自己的CPU和数据缓存资源对数据发送和接收能力来进行动态调整,从而灵活性更强,也更合理。

三次握手协议

  • 在利用TCP实现源主机和目的主机通信时,目的主机必须同意,否则TCP连接无法建立。为了确保TCP连接的成功建立,TCP采用了一种称为三次握手的方式,三次握手方式使得“序号/确认号”系统能够正常工作,从而使它们的序号达成同步。如果三次握手成功,则连接建立成功,可以开始传送数据信息。

  • 其三次握手分别为:

1)源主机A的TCP向主机B发送连接请求报文段,其首部中的SYN(同步)标志位应置为1,表示想跟目标主机B建立连接,进行通信,并发送一个同步序列号X(例:SEQ=100)进行同步,表明在后面传送数据时的第一个数据字节的序号为X+1(即101)。

2)目标主机B的TCP收到连接请求报文段后,如同意,则发回确认。再确认报中应将ACK位和SYN位置为1.确认号为X+1,同时也为自己选择一个序号Y。

3)源主机A的TCP收到目标主机B的确认后要想目标主机B给出确认。其ACK置为1,确认号为Y+1,而自己的序号为X+1。TCP的标准规定,SYN置1的报文段要消耗掉一个序号。

  • 运行客户进程的源主机A的TCP通知上层应用进程,连接已经建立。当源主机A向目标主机B发送第一个数据报文段时,其序号仍为X+1,因为前一个确认报文段并不消耗序号。

  • 当运行服务进程的目标主机B的TCP收到源主机A的确认后,也通知其上层应用进程,连接已经建立。至此建立了一个全双工的连接。

  • 三次握手为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。

套接字接口

  • 套接字接口 (socket interface) 是一组函数,它们和 Unix I/O 函数结合起来,用以创建网络应用。

  • 套接字接口描述:

  • Linux 系统是通过提供套接字(socket)来进行网络编程的.网络程序通过socket和其它几个函数的调用,会返回一个通讯的文件描述符,我们可以将这个描述符看成普通的文件的描述符来操作,这就是linux的设备无关性的 好处.我们可以通过向描述符读写操作实现网络之间的数据交流.

  • socket

    int socket(int domain, int type,int protocol)

domain: 说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等). AF_UNIX只能够用于单一的Unix系统进程间通信,而AF_INET是针对Internet的,因而可以允许在远程 主机之间通信(当我们 man socket时发现 domain可选项是 PF_而不是AF_,因为glibc是posix的实现 所以用PF代替了AF,不过我们都可以使用的).

type:我们网络程序所采用的通讯协议(SOCK_STREAM, SOCK_DGRAM等) SOCK_STREAM表明我们用的是TCP协议,这样会提供按顺序的,可靠,双向,面向连接的比特流. SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信.

protocol:由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了 socket为网络通讯做基本的准备.成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况。

  • 其他socket信息数据结构
	
	struct sockaddr
	{
     unsigned short sa_family; /*地址族*/
     char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号。*/
	};




struct sockaddr_in
{
     short int sa_family; /*地址族*/
     unsigned short int sin_port; /*端口号*/
     struct in_addr sin_addr; /*IP地址*/
     unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/
};




struct in_addr
{
unsigned long int  s_addr; /* 32位IPv4地址,网络字节序 */
};


  • 头文件<netinet/in.h>

    sa_family:AF_INET àIPv4协议 AF_INET6 àIPv6协议

  • bind
    int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
    sockfd:是由socket调用返回的文件描述符.

addrlen:是sockaddr结构的长度.

my_addr:是一个指向sockaddr的指针. 在中有 sockaddr的定义
struct sockaddr{
unisgned short as_family;
char sa_data[14];
};

不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替.在中有sockaddr_in的定义
struct sockaddr_in{
unsigned short sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];

我们主要使用Internet所以sin_family一般为AF_INET,sin_addr设置为INADDR_ANY表示可以 和任何的主机通信,sin_port是我们要监听的端口号.sin_zero[8]是用来填充的. bind将本地的端口同socket返回的文件描述符捆绑在一起.成功是返回0,失败的情况和socket一样

  • listen
    int listen(int sockfd,int backlog)

sockfd:是bind后的文件描述符.

backlog:设置请求排队的最大长度.当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度. listen函数将bind的文件描述符变为监听套接字.返回的情况和bind一样.

  • accept
    int accept(int sockfd, struct sockaddr *addr,int *addrlen)
    sockfd:是listen后的文件描述符.

addr, addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了. bind,listen和

accept是服务器端用的函数,accept调用时,服务器端的程序会一直阻塞到有一个 客户程序发出了连接.

accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了. 失败时返回-1

  • connect
    int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
    sockfd:socket返回的文件描述符.

serv_addr:储存了服务器端的连接信息.其中sin_add是服务端的地址

addrlen:serv_addr的长度

connect函数是客户端用来同服务端连接的.成功时返回0,sockfd是同服务端通讯的文件描述符 失败时返回-1.

客户端-服务器编程模型

  • 每个网络应用都是基于客户端一服务器模型的。一个应用是由一个服务器进程和一个或者多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务。
    客户端一服务器模型中的基本操作是事务,由四步组成:

    当一个客户端需要服务时,它向服务器发送一个请求,发起一个事务。
    服务器收到请求后,解释它,并以适当的方式操作它的资源。
    服务器给客户端发送一个响应,并等待下一个请求。
    客户端收到响应并处理它。

  • 和C语言教程一样,从一个简单的“Hello World!”程序切入 socket 编程。

此代码实现的功能是:客户端从服务器读取一个字符串并打印出来。

  • 服务器端代码 server.cpp:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//进入监听状态,等待用户发起请求
listen(serv_sock, 20);
//接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
//向客户端发送数据
char str[] = "Hello World!";
write(clnt_sock, str, sizeof(str));
//关闭套接字
close(clnt_sock);
close(serv_sock);
return 0;
}

  • 客户端代码 client.cpp:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
//向服务器(特定的IP和端口)发起请求
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//读取服务器传回的数据
char buffer[40];
read(sock, buffer, sizeof(buffer)-1);
printf("Message form server: %s
", buffer);
//关闭套接字
close(sock);
return 0;
}


  • 运行结果如下:

  • 这是一个简单的程序,client 运行后,通过 connect() 函数向 server 发起请求,处于监听状态的 server 被激活,执行 accept() 函数,接受客户端的请求,然后执行 write() 函数向 client 传回数据。client 接收到传回的数据后,connect() 就运行结束了,然后使用 read() 将数据读取出来。

  • 如果想要能实现传输文件这样的功能,需要客户端首先输入文件名,并判断该文件是否存在。若不存在,则返回错误;若存在,则发送至服务器。服务器接收到数据后,创建一个同样名称的文件。文件名称一般很短,但文件内容大小并不确定。所以可能一次性传输不完所有的数据,应该设置循环,以固定大小传输数据,一直到传输结束。

  • 增添修改部分代码如下:

服务器:


	char file_name[FILE_NAME_MAX_SIZE+1];
    bzero(file_name, FILE_NAME_MAX_SIZE+1);
    char buffer[BUFFER_SIZE];
    bzero(buffer,BUFFER_SIZE);
    recv(new_server_socket,file_name,BUFFER_SIZE,0);   
    FILE * fp = fopen(file_name,"w");
    if(NULL == fp )
    {
        printf("File:	%s Can Not Open To Write
", file_name);
        exit(1);
    }    
    //从客户端接收数据到buffer中
    bzero(buffer,BUFFER_SIZE);
    int len = 0;
    while( len = recv(new_server_socket,buffer,BUFFER_SIZE,0))
    {
        if(len < 0)
        {
            printf("Recieve Data From Client %s Failed!
", argv[1]);
            break;
        }
        int write_length = fwrite(buffer,sizeof(char),len,fp);
        if (write_length<len)
        {
            printf("File:	%s Write Failed
", file_name);
            break;
        }
        bzero(buffer,BUFFER_SIZE);    
    }
    printf("File:	%s Transfer Finished!
",file_name);  
    fclose(fp);

客户端:


	char file_name[FILE_NAME_MAX_SIZE+1];
    bzero(file_name, FILE_NAME_MAX_SIZE+1);
    printf("Please Input File Name On Server:	");
    scanf("%s", file_name);
    char buffer[BUFFER_SIZE];
    bzero(buffer,BUFFER_SIZE);
    strncpy(buffer, file_name, strlen(file_name)>BUFFER_SIZE?BUFFER_SIZE:strlen(file_name));
    //向服务器发送buffer中的数据
    send(client_socket,buffer,BUFFER_SIZE,0);
    FILE * fp = fopen(file_name,"r");
    if(NULL == fp )
    {
        printf("File:	%s Not Found
", file_name);
        exit(1);
    }
    else
    {
            bzero(buffer, BUFFER_SIZE);
            int file_block_length = 0;
            while( (file_block_length = fread(buffer,sizeof(char),BUFFER_SIZE, fp))>0)
            {
                //printf("file_block_length = %d
",file_block_length);
                //发送buffer中的字符串到服务器
                if(send(client_socket,buffer,file_block_length,0)<0)
                {
                    printf("Send File:	%s Failed
", file_name);
                    break;
                }
                bzero(buffer, BUFFER_SIZE);
            }
    }    
    printf("Send File:	 %s To Server[%s] Finished
",file_name, argv[1]);  
    printf("The File has %d words.
", wc_func(file_name));
    fclose(fp);


  • 像实验三任务一一样,基于Linux Socket程序设计实现wc(1)服务器(端口号是你学号的后6位)和客户端。运行结果如下:

  • 第八周课后作业基于socket使用教材的csapp.h csapp.c,实现daytime(13)服务器(端口我们使用13+后三位学号)和客户端。

运行结果如下:

  • 但是迭代网络服务器是不可现实的,因为他们一次只能为一个客户端提供服务。一个更好的方法是创建一个并发服务器,他为每个客户端创建一个单独的逻辑流。这就允许服务器同时为多个客户端提供服务,而且也避免了慢速客户端独占服务器。

第十一章 并发

  • 三种基本的构造并发程序的方法:

    进程:

      每个逻辑控制流是一个进程,由内核进行调度,进程有独立的虚拟地址空间
    

    I/O多路复用:

      逻辑流被模型化为状态机,所有流共享同一个地址空间
    

    线程:

      运行在单一进程上下文中的逻辑流,由内核进行调度,共享同一个虚拟地址空间
    

基于进程的并发服务器

1.使用SIGCHLD处理程序来回收僵死子进程的资源。

2.父进程必须关闭他们各自的connfd拷贝(已连接的描述符),避免存储器泄露。

3.因为套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。

注意:

1.父进程需要关闭它的已连接描述符的拷贝(子进程也需要关闭)

2.必须要包括一个SIGCHLD处理程序来回收僵死子进程的资源

3.父子进程之间共享文件表,但是不共享用户地址空间。

关于独立地址空间

优点:防止虚拟存储器被错误覆盖

缺点:开销高,共享状态信息才需要IPC机制

  • TCP 并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是由服务器创建一个子进程来处理。

  • 使用fork函数实现多进程并发服务器:

  • fork 调用后,父进程和子进程继续执行 fork 函数后的指令,是父进程先执行还是子进程 先执行是不确定的,这取决于系统内核所使用的调度算法。
      而在网络编程中,父进程中调用 fork 之前打开的所有套接字描述符在函数 fork 返回之后都是共享。如果父、子进程同时对同一个描述符进行操作, 而且没有任何形式的同步,那么它们的输出就会相互混合。

- fork函数在并发服务器中的应用:
  父、子进程各自执行不同的程序段,这是非常典型的网络服务器。父进程等待客户 的服务请求。当这种请求到达时,父进程调用 fork 函数,产生一个子进程,由子进程对该请求作处理。父进程则继续等待下一个客户的服务请求。并且这种情况下,在 fork 函数之后,父、子进程需要关闭各自不使用的描述符,即父进程将不需要的 已连接描述符关闭,而子进程关闭不需要的监听描述符。这么做的原因有3个:

节省系统资源
防止上面提到的父、子进程同时对共享描述符进程操作
最重要的一点,是确保close函数能够正确关闭套接字描述符

我们在socket编程中调用 close 关闭已连接描述符时,其实只是将访问计数值减 1。而描述符只在访 问计数为 0 时才真正关闭。所以为了正确的关闭连接,当调用 fork 函数后父进程将不需要的 已连接描述符关闭,而子进程关闭不需要的监听描述符。

  • 编写多进程并发服务器的基本思路:

    建立连接
    服务器调用fork()产生新的子进程
    父进程关闭连接套接字,子进程关闭监听套接字
    子进程处理客户请求,父进程等待另一个客户连接。

  • daytime服务器多进程实现成并发服务器并测试

测试代码:

ser:


#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<ctype.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<errno.h>
#include<errno.h>
#include <time.h>

#define SERV_PORT 13321

int std_err(const char* name)
{
    perror(name);
	exit(1);
}

int main(void)
{
	int sfd, cfd, ret;
	int len;
	pid_t pid;
	socklen_t clie_len;
	char buf[BUFSIZ], clibuf[32]; //创建服务器套节字
	sfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sfd == -1)
	std_err("socket"); //定义地址类型
	struct sockaddr_in serv_addr, clie_addr;
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//绑定服务器的IP、端口;
	ret = bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	if(ret == -1)
	std_err("bind");//监听链接服务器的客户数量
	ret = listen(sfd, 3);
	if(ret == -1)
		std_err("listen");
	clie_len = sizeof(clie_addr);
	 while(1)//阻塞等待客户端发起链接请求
	{
	cfd = accept(sfd, (struct sockaddr*)&clie_addr, &clie_len);
	printf("服务器实现者20155326	");
	printf("客户端IP:%s
",inet_ntoa(clie_addr.sin_addr)); 
	time_t t = time(0); 
	char tmp[64]; 
	strftime( tmp, sizeof(tmp), "%Y/%m/%d %X %A
	", localtime(&t) ); //这里无需打印当前时间,应该是客户端打印当前时间
	if(cfd == -1)
{
	std_err("accept");
	pid = fork();
	if(pid < 0)
	std_err("fork:");
}
else if(pid == 0)
{
	close(sfd);
	break;
}
 else        //住进程实现逻辑;1.回收子进程,2,关闭不必要的文件描述父 3,继续等待客户端链接,如果有,则继续创建子进程
	{
	send(cfd,tmp,strlen(tmp),0);
	close(cfd);
	}
 }
	return 0;
	}


cli:


#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<strings.h>
#include<ctype.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<errno.h>
#include<pthread.h>
#include <pthread.h>
#define SERV_PORT 13321
#define SERV_IP "127.0.0.1"
#define NUM 3
int std_err(const char* name)
{
	perror(name);
	exit(1);
}
int main(void)
{
	int cfd, ret;
	char buf[BUFSIZ];
	pid_t pid;
	int i;
	for(i = 0; i < NUM; i++){
		pid = fork();
		if(pid == 0)
		break;
		else if(pid < 0)
			std_err("fork");
			}
//子进程逻辑
	if(pid == 0)
	{//创建套节字
		cfd = socket(AF_INET, SOCK_STREAM, 0);
		if(cfd == -1)
		std_err("socket");
//定义IP , 端口
		struct sockaddr_in clie_addr;
		clie_addr.sin_family = AF_INET;
		clie_addr.sin_port = htons(SERV_PORT);//转换IP 字符串的地址
		ret = inet_pton(AF_INET, SERV_IP, &clie_addr.sin_addr.s_addr);
		if(ret != 1)
		std_err("inet_pton");
//链接服务器
		ret = connect(cfd, (struct sockaddr*)&clie_addr, sizeof(clie_addr));
		if(ret == -1)
		std_err("connect");
		char buff[256];
		int nRecv=recv(cfd,buff,256,0);
		if(nRecv>0)
		{   
		buff[nRecv]='';
		 printf("当前时间:%s
",buff);									} 
	} //关闭套节字
	close(cfd);
	return 0;
}


基于I/O多路复用的并发编程

I/O多路复用技术使用select函数要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
select函数处理类型为fd_set的集合,即描述符集合,并在逻辑上描述为一个大小为n的位向量,每一位b[k]对应描述符k,但当且仅当b[k]=1,描述符k才表明是描述符集合的一个元素。

描述符能做的三件事: 1、分配他们 2、将一个此种类型的变量赋值给另一个变量 3、用FDZERO、FDSET、FDCLR和FDISSET宏指令来修改和检查它们

当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符k表示准备好可以读了
我们必须在每次调用select时都更新读集合
事件驱动程序:将逻辑流模型化为状态机。
一个状态机就是一组状态、输入事件和转移,其中转移就是将状态和输入事件映射到状态。

  • 基于I/O多路复用的并发事件驱动服务器的流程如下:

select函数检测到输入事件
add_client函数创建新状态机
check_clients函数执行状态转移(在课本的例题中是回送输入行),并且完成时删除该状态机。
基于线程的并发编程

  • 利用I/O多路复用方式实现echo服务器

基于线程的并发编程

  • 线程执行模型

每个进程开始生命周期时都是单一线程(主线程),在某一时刻创建一个对等线程,从此开始并发地运行,最后,因为主线程执行一个慢速系统调用,或者被中断,控制就会通过上下文切换传递到对等线程。

  • Posix线程

Posix线程是C语言中处理线程的一个标准接口,允许程序创建、杀死和回收线程,与对等线程安全的共享数据。

线程的代码和本地数据被封装在一个线程例程中,

  • 创建线程

线程通过调用pthread_create来创建其他线程。

int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg);

成功则返回0,出错则为非零

当函数返回时,参数tid包含新创建的线程的ID,新线程可以通过调用pthread_self函数来获得自己的线程ID。

pthread_t pthread_self(void);

返回调用者的线程ID。

  • 终止线程
    一个线程是通过以下方式之一来终止的。

当顶层的线程例程返回时,线程会隐式地终止。
通过调用pthread_exit函数,线程会显式地终止

void pthread_exit(void *thread_return);

回收已终止的线程资源
线程通过调用pthread_join函数等待其他线程终止。

int pthread_join(pthread_t tid,void **thread_return);

成功则返回0,出错则为非零

  • 分离线程
    在任何一个时间点上,线程是可结合或可分离的。一个可结合的线程能够被其他线程收回其资源和杀死,在被回收之前,它的存储器资源是没有被释放的。分离的线程则相反,资源在其终止时自动释放。

    int pthread_deacth(pthread_t tid);
    成功则返回0,出错则为非零

初始化线程
pthread_once允许初始化与线程例程相关的状态。

pthread_once_t once_control=PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_contro,
void (*init_routine)(void));

总是返回0

  • 互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。

  • 举一个例子:


#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define NLOOP 5000

int counter;

pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *doit( void * );

int main(int argc, char **argv)
{
	pthread_t tidA, tidB;

	pthread_create( &tidA ,NULL, &doit, NULL );
	pthread_create( &tidB ,NULL, &doit, NULL );

	pthread_join( tidA, NULL );
	pthread_join( tidB, NULL );

	return 0;
}

void * doit( void * vptr)
{
	int i, val;

	for ( i=0; i<NLOOP; i++ ) {
		pthread_mutex_lock( &counter_mutex );
		val = counter++;
		printf("%x: %d 
", (unsigned int) pthread_self(), val + 1);
		counter = val + 1;
		pthread_mutex_unlock( &counter_mutex );
	}
	return NULL;
}

运行结果:

  • 程序首先定义了一个宏PTHREADMUTEXINITIALIZER来静态初始化互斥锁。先创建tidA线程后运行doit函数,利用互斥锁锁定资源,进行计数,执行完毕后解锁。后创建tidB,与tidA交替执行。由于定义的NLOOP值为5000,所以程序最后的输出值为10000.程序的最后还需要分别回收tidA和tidB的资源。

    pthread_creat:创建线程,若成功则返回0,若失败则返回出错编号。第一个参数为指向线程标识符的指针,创建成功时指向的内存单元被设置为新创建线程的线程ID;第二个参数设置线程属性;第三个参数是线程运行函数的起始地址;最后一个参数是运行函数的参数

    pthread_join:用来等待一个线程的结束。当函数返回时,被等待线程的资源被收回。

    pthreadmutexlock:线程调用该函数让互斥锁上锁。成功锁定时返回0,其他任何返回值都表示出现了错误。

    pthreadmutexunlock:与pthreadmutexlock成对存在。释放互斥锁。

  • 若是要用多线程实现传送文本文件的服务器和客户端

则和上面的第二个例子相比,服务器需要循环检测是否有新的连接。如果有,则调用pthread_create()函数创建新的进程,并执行以下代码:


while(1){
        //接受客户端连接
        socklen_t addrlen = sizeof(struct sockaddr);
        struct sockaddr_in client_addr; //客户端地址结构
        int client_sock = accept(ss, (struct sockaddr*)&client_addr, &addrlen);
        if(client_sock < 0){
            printf("accept error
");
        }
        printf("accept success
");

        pthread_t pid;
        if(pthread_create(&pid, NULL, process_client, &client_sock) < 0){
            printf("pthread_create error
");
        }
    }


代码运行结果如下:

  • 把第八周课上练习3的daytime服务器用多线程实现成并发服务器并测试
    cli:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<ctype.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/wait.h>
#include<signal.h>
#include<errno.h>
#include<pthread.h>
#include <pthread.h>

#define SERV_PORT 13321
#define SERV_IP "127.0.0.1"
#define NUM 3

int std_err(const char* name)
{
    perror(name);
	    exit(1);
}
int main(void)
{
	int cfd, ret;
	char buf[BUFSIZ];
	pid_t pid;
	int i;
	//创建套节字
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	if(cfd == -1)
		std_err("socket");
	//定义IP , 端口
	struct sockaddr_in clie_addr;
	clie_addr.sin_family = AF_INET;
	clie_addr.sin_port = htons(SERV_PORT);
	//转换IP 字符串的地址
	ret = inet_pton(AF_INET, SERV_IP, &clie_addr.sin_addr.s_addr);
	if(ret != 1)
		std_err("inet_pton");
	//链接服务器
	ret = connect(cfd, (struct sockaddr*)&clie_addr, sizeof(clie_addr));
	if(ret == -1)
	std_err("connect");
	char buff[256];
	int nRecv=recv(cfd,buff,256,0);
	if(nRecv>0)
	{   
		buff[nRecv]='';
		printf("当前时间:%s
",buff);
	} 
	
	//关闭套节字
	close(cfd);
	return 0;

}


ser:



#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<sys/socket.h>
#include<sys/types.h>       //pthread_t , pthread_attr_t and so on.
#include<netinet/in.h>      //structure sockaddr_in
#include<arpa/inet.h>       //Func : htonl; htons; ntohl; ntohs
#include<assert.h>          //Func :assert
#include<string.h>          //Func :memset
#include<unistd.h>          //Func :close,write,read
#include<ctype.h>
#include<arpa/inet.h>
#include <time.h>
#define SOCK_PORT 9988
#define BUFFER_LENGTH 1024
#define MAX_CONN_LIMIT 5    //MAX connection limit
static void Data_handle(void * sock_fd);
struct sockaddr_in s_addr_in;
struct sockaddr_in s_addr_client;
int sockfd;
int main()
{
	int sockfd_server;
	int fd_temp;
	int client_length;
	sockfd_server = socket(AF_INET,SOCK_STREAM,0);  //ipv4,TCP
	assert(sockfd_server != -1);//before bind(), set the attr of structure sockaddr.
	memset(&s_addr_in,0,sizeof(s_addr_in));
	s_addr_in.sin_family = AF_INET;
	s_addr_in.sin_addr.s_addr = htonl(INADDR_ANY);  //trans addr from uint32_t host byte order to network byte order.
	s_addr_in.sin_port = htons(SOCK_PORT);          //trans port from uint16_t host byte order to network byte order.
	fd_temp = bind(sockfd_server,(struct scokaddr*)(&s_addr_in),sizeof(s_addr_in));
	if(fd_temp == -1)
	{
		fprintf(stderr,"bind error!
");
		 exit(1);
	 }
	fd_temp = listen(sockfd_server,MAX_CONN_LIMIT);
	if(fd_temp == -1)
	{
		fprintf(stderr,"listen error!
");
		exit(1);
	}
	 while(1)
	{
		//printf("waiting for new connection...
");
		pthread_t thread_id;
		client_length = sizeof(s_addr_client);//Block here. Until server accpets a new connection.
		sockfd = accept(sockfd_server,(struct sockaddr*)(&s_addr_client),&client_length);
		time_t t = time(0); 
		char tmp[64]; 
		strftime( tmp, sizeof(tmp), "%Y/%m/%d %X %A
	", localtime(&t) ); 
		send(sockfd,tmp,strlen(tmp),0); 
		close(sockfd);  //close a file descriptor.
		if(sockfd == -1)
		{
			fprintf(stderr,"Accept error!
");
			continue; //ignore current socket ,continue while loop.
		}
		if(pthread_create(&thread_id,NULL,(void *)(&Data_handle),(void *)(&sockfd)) == -1)
		{
			fprintf(stderr,"pthread_create error!
");
			break;//break while loop
		}
		}//Clear
	int ret = shutdown(sockfd_server,SHUT_WR); //shut down the all or part of a full-duplex connection.
	assert(ret != -1);
	return 0;
}

static void Data_handle(void * sock_fd)
{
	int fd = *((int *)sock_fd);
	int i_recvBytes;
	char data_recv[BUFFER_LENGTH];
	printf("服务器实现者20155326	");
	printf("客户端IP:%s
",inet_ntoa(s_addr_client.sin_addr)); 
	pthread_exit(NULL);   //terminate calling thread!
}


学习过程中遇到的问题及解决方法

  • 11.1

  • 11.2
    编写了如下代码,将十六进制参数转换为点分十进制串。并验证了11-1的答案。
    #include "csapp.h"
    int main(int argc,char **argv)
    {
    struct in_addr inaddr;
    uint32_t addr;
    char buf[MAXBUF];
    if(argc !=2){
    fprintf(stderr,"usage:%s<hex number>
",argv[0]);
    exit(0);
    }
    sscanf(argv[1],"%x",&addr);
    inaddr.s_addr=htonl(addr);
    if(!inet_ntop(AF_INET,&inaddr,buf,MAXBUF))
    unix_error("inet_ntop");
    printf("%s
",buf);
    exit(0);
    }


  • 11.3编写了如下代码,将点分十进制串转换为十六进制参数。并验证了11-1的答案。

    #include "csapp.h"
    int main(int argc,char **argv){
    struct in_addr inaddr;
    int rc;
    if(argc!=2)
    {
    fprintf(stderr,"usafe:%s<dotted-decimak>
",argv[0]);
    
    }
    rc=inet_pton(AF_INET,argv[1],&inaddr);
    if(rc==0)
    app_error("inet_pton error:invalid dotted-decimal address");
    else if(rc<0)
    unix_error("inet_pton error");
    printf("0x%x
",ntohl(inaddr.s_addr));
    exit(0);
    }

  • 11.4用inet_pton而不是getnameinfo将每个套接字地址转换成点分十进制地址字符串。

    #include "csapp.h"
    
    int main(int argc,char **argv)
    {
    struct addrinfo *p,*listp,hints;
    struct sockaddr_in *sockp;
    char buf[MAXLINE];
    int rc;
    if(argc!=2){
    fprintf(stderr,"usage:%s<domain name>
",argc[0]);
    exit(0);
    }
    meset(&hints,0,sizeof(struct addrinfo));
    hints.ai_family=AF_INET;
    hints.ai_socktype=SOCK_STREAM;
    if((rc=gataddrinfo(argv[1],NULL,&hints,&listp))!=0){
    fprintf(stderr,"getaddrinfo error :%s
",gai_strerror(rc));
    exit(1);
    
    }
    for(p=listp;p;p=p->ai_next){
    sockp=(struct sockaddr_in *)p->ai_addr;
    Inet_ntop(AF_INET,&(sockp->sin_addr),buf,MAXLINE);
    printf("%s
",buf);
    }
    Freeaddrinfo(listp);
    exit(0);
    }

  • 11.5

在网络应用中使用C标准I/O函数会有危险,但是在下图中的CGI程序却没有任何问题的使用标准I/O,为什么呢?

回答:在CGI中应用标准I/O时,子进程中运行的CGI程序不用显式地关闭输入输出流,当子进程结束时内核会自动关闭所有描述符。

  • 12.1

当父进程派生子进程时,它得到一个已连接描述符的副本,并将相关文件表中的引用计数从1增加到2.当父进程关闭它的描述符副本时,引用计数就从2减少到1.因为内核不会关闭一个文件,知道文件表中它的引用计数值变为0,所以子进程这边的连接端将保持打开。

  • 12.2

当一个进程因为某种原因终止时,内核将关闭所有打开的描述符。因此,当子进程退出时,它的已连接文件描述符的副本也将被自动关闭。

  • 12.3

如果一个从描述符中读一个字节的请求不会阻塞,那么这个描述符就准备好可以读了。假如EOF在一个描述符上为真,那么描述符也准备好可读了,因为读操作将立即返回一个零返回码,表示EOF。因此,键入Ctrl+D会导致select函数返回,准备好的集合中有描述符0.

  • 12.4

因为变量pool.read_set既作为输入参数,也作为输出参数,所以我们在每一次调用select之前都重新初始化它。在输入时,它包含读集合。在输出时,它包含准备好的集合。

  • 12.5

因为线程运行在同一个进程中,它们都共享相同的描述符表。无论有多少线程使用这个已连接描述符,这个已连接描述符的文件表的引用计数都等于1.因此,当我们用完它时,一个close操作就足以释放于这个已连接描述符相关的内存资源了。

  • 12.6

  • 12.7

  • 12.8

  • 12.9

  • 12.10

假设一个特殊的信号量实现为每一个信号量使用了一个LIFO的线程栈。当一个线程在P操作中阻塞在一个信号量上,它的ID就被压入栈中。类似地,V操作从栈中弹出栈顶的线程ID,并重启这个线程。根据这个栈的实现,一个在它的临界区中竞争的写者会简单的等待,直到在他释放这个信号量之前另一个写者阻塞在这个信号量上。在这种场景中,当两个写者来回地传递控制权时,正在等待的读者可能会永远的等待下去。

  • 12.11

  • 12.12

ctime_ts函数不是可重入函数,因为每次调用都共享相同的由ctime函数返回的static变量。然而,它是线程安全的,因为对共享变量的访问是被P和V操作保护的,因此是互斥的。

  • 12.13

如果在第4行刚调用完pthread_create后就释放内存,这回引起一个竞争,这个竞争发生在主线程对free的调用和24的行的赋值语句之间。

  • 12.14

A.另一种方法是直接传递整数i,而不是传递一个指向i的指针:

for(i=0;i<N;i++)
Pthread_create(&tid[i],NULL,thread,(void*)i);

在线程例程中,我们将参数强制转换成一个int型变量,并将它赋值给myid;

int myid=(int)vargp;

B.优点是它通过消除对malloc和free的调用降低了开销。一个明显的缺点是,它假设指针至少和int一样大。即便这种假设对于所有得现代系统来说都为真,但是它对于那些过去遗留下来的或今后的系统来说可能就不为真了。

第12章错题

1、有关线程图,下面说法正确的是()

A.图的原点表示没有任何线程完成一条指令的初始状态

B.向右向上是合法的转换

C.向左向下是合法的转换

D.对角线是合法的转换

E.一个程序执行的历史被模型化成状态空间中的一条轨迹线

F.进度图中,两个临界区的交集形成不安全区

正确答案: A B E F 你的答案: A B E

解析:p704。 合法的转换是向右或者向上,对角线转换是不允许的。

2、有关“生产者-消费者”和“读者-写者”模型,下面说法正确的是()

A.二者除处理的都是互斥问题

B.二者除处理的都是同步问题

C.二者都要保证对缓冲区的访问是互斥的

D.“生产者-消费者”模型要保证对缓冲区的访问是互斥的

E.“读者-写者”模型要保证读者对缓冲区的访问是互斥的

正确答案: B D 我的答案: A D

解析:p704。读者-写者问题是互斥问题的一个概括。生产者和消费者线程共享一个有n个槽的有限缓冲区。

3、有关下面的代码hello.c,编译后的可执行程序为phello,下面说法正确的是()

A.编译命令是:gcc hello.c -o phello

B.编译命令是:gcc hello.c -lpthread -o phello

C.编译命令是:gcc hello.c -pthread -o phello

D.phello运行时有一个线程

E.phello运行时有两个线程

F.phello运行时主线程先执行完

G.phello运行时对等线程先执行完

H.phello运行时对等线程和主线程执行顺序不确定

正确答案: B C E G 我的答案: B E G

解析:多线程编译需要-lpthread或-pthread参数,pthread_join使得主线程等待对等线程先执行完

4、有关下面代码,编译后的可执行程序是echoserv,下面说法正确的是()

A.第19行中的STDIN_FILENO的值可以用grep -nr STDIN_FILENO /usr/include 查到为1

B.第24行select()会导到致程序阻塞,可以替代accept()

C.程序运行时,输入CTRL+D,可以让select返回

D.以上代码中加入csapp.h就能编译成功

正确答案: C 我的答案: A B C D

解析:p686。grep -nr STDIN_FILENO /usr/include STDIN_FILENO为0。该图展示了可以如何利用select来实现一个迭代echo服务器,它也可以接受标准输入上的用户命令。

5、有关下面代码,编译后的可执行程序是echoserv,下面说法正确的是()

A.这是一个并发echo服务器

B.这是一个迭代echo服务器

C.第33行关闭已连接描述符

D.第33行关闭监听描述符

E.删除第33行会导致内存泄露

F../echoserv 5056 ,5056是连接的客户端进程的端口号

G../echoserv 5056 ,5056是连接的服务器进程的端口号

H.删除第30行会导致内存泄露

正确答案: A C E G 我的答案: A C E F

解析:p682,习题12.2

代码量

码云链接

结对及互评

-本周结对学习情况

20155320

同伴重点学习了第十二章的教材内容,刚好和我一样,我复习了第十一、十二章的内容。

其他(感悟、思考等)

这周的任务是学习自己认为最重要的一章,我重新学习了网络编程和并发,让我更加理解了书中的内容,也结合了之前的一些实践内容做了总结。

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 0/0 1/1 10/20
第二周 57/100 1/1 20/30
第三周 100/100 1/1 30/30
第四周 233/200 1/1 20/20
第五周 267/200 1/1 20/20
第六周 220/2000 3/1 40/20
第七周 362/300 1/1 30/30
第八周 3000/300 3/1 45/30
第九周 596/300 3/1 45/30
第十周 154/300 3/1 40/30
第十一周 154/300 3/1 40/30
第十二周 343/300 3/1 44/30
第十三周 963/300 1/1 35/30

尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。

耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。

参考:软件工程软件的估计为什么这么难,软件工程 估计方法

  • 计划学习时间:25小时
  • 实际学习时间:20小时
  • 改进情况:
    (有空多看看现代软件工程 课件
    软件工程师能力自我评价表)

参考资料

  • 《深入理解计算机系统V3》学习指导
  • ...
原文地址:https://www.cnblogs.com/lmc1998/p/8051942.html