网络编程笔记

xdu计软网络编程的笔记,任课教师里白肚抚(张彤)。
这门课锐评一下吧,不应该作为单独的专业课来学,都是面向函数文档编程。更不应该有期末考试,考一堆八股文下来,真正会写代码的考不了好分数,反而是做实验时连服务器代码都跑不起来的死记硬背战士能考高分,纯粹是笑话(倒也是你电特色笑话)。
本文章中所有代码都是能在ubuntu18.04虚拟机上跑的,部分为张老师课件中的代码,部分为本鼠自己写的(例如inetd守护进程服务器部分)。祝考前抱佛脚的学弟们看了本文章能一次性考过。如发现文章中的问题,请评论指出,本鼠看到立马改正。


unit 02 流式套接字

流程图

image

基本函数

1. socket

创建一个套接字描述符

int socket (int family, int type, int protocol);
input:
	family(协议簇):AF_INET(一般是这个)、AF_UNIX
	type(类型):SOCK_STREAM(流式)、SOCK_DGRAM(数据报式)、SOCK_RAW
	protocol(协议):默认为0
output:
	返回套接字描述符。错误的话返回-1,系统全局变量errno为错误代码

2. 地址结构体

struct sockaddr:给计算机用

struct sockaddr {
	/*地址类型AF_xxx,一般为AF_INET*/
	 u_short    sa_family;
 	/*协议地址,不同的协议地址格式不同,储存IP和port*/ 
	char	    sa_data[14];
}; 

struct sockaddr_in:给程序员用

struct sockaddr_in {
        short 		sin_family;  /*AF_INET,HBO顺序*/
        u_short 		sin_port;     /*端口号,网络字节顺序NBO*/
        struct  in_addr 	sin_addr;    /*IP地址,网络字节顺序NBO*/
        char    		sin_zero[8]; /*填充字节,必须为全零*/
};

struct in_addr {  	
	u_long   S_addr;	//表示32位IP,采用NBO顺序
};

3. HBO --- NBO互转

unsigned short int htons(unsigned short int hostshort)	//port转换
unsigned long int htonl(unsigned long int hostlong)		//IP转换
unsigned short int ntohs(unsigned short int netshort)
unsigned long int ntohl(unsigned long int netlong)

4. IP地址转换(char与int)

//原型
//字符串转int,成功返回非0值,失败返回0
int inet_aton(const char *cp,struct in_addr *inp);
//int转字符串,注意,会把转换得到的字符串保存到一块固定的内存中
char* inet_ntoa(struct in_addr in);

//字符串形式地址转换为网络地址形式
struct sockaddr_in addr;
inet_aton(“219.245.78.159”,&addr.sin_addr);
//网络地址转换为字符串地址形式
printf(“%s”,inet_ntoa(addr.sin_addr);

5. bind

将套接字与端口绑定

int bind(int sockfd,struct sockaddr *myaddr,int addrlen); 
input:
    sockfd-socket描述符
	myaddr-自己的地址(IP和port)
	addrlen-地址结构体的size
output:
    0表示绑定成功,-1表示失败

客户端不需要使用套接字绑定到端口(因为不需要创建听套接字来监听端口)

若IP地址设置为INADDR_ANY,则会自动设置为本机的任意IP(适用于多网卡主机)

6. listen

使套接字监听本地端口,将连接请求储存在队列中(相当于已经与client建立了一个连接,但还没有收发数据)

int listen(int sockfd,int backlog);
input:
    sockfd-已绑定到某一端口的的socket描述符
    backlog-已完成连接,等待接受的队列最大长度
output:
    0-成功,-1-失败

7. accept

只用于server,接受一个连接请求(接受前所有的连接保存在一个队列中),创建用于连接的套接字。

int accept(int sockfd,struct sockaddr *clientaddr,int addrlen);
input:
    sockfd-听套接字的描述符
	clientaddr-地址结构体。(1)只经过初始化,accept结束后被系统自动填入client的地址 (2)置为NULL
	addrlen-地址结构体的size。若clientaddr置为NULL,则addrlen也置为NULL
output:
    成功返回连接套接字的描述符,失败返回-1

注:accept函数在没有已完成的连接时将阻塞进程

8. connect

默认为阻塞方式工作。成功连接或出错时返回

只用于client,连接到server。

int connect(int sockfd,struct sockaddr *servaddr,int* addrlen);
input:
    sockfd-socket描述符
    servaddr-地址结构体,保存目标server地址
    addrlen-地址结构体的size
output:
    0-成功,-1-失败

注:对一个socket描述符不能两次使用connect函数

若client使用connect连接server时,server还未启动,就会连接失败返回-1(对应下面的情况2)

出错返回的三种情况:

若TCP客户没有收到SYN分节的相应,则返回ETIMEDOUT错误。举例,调用connect函数时,4.4BSD内核发送一个SYN,若无响应则等待6s后再发送一个,若仍无响应则等待24s后再发送一个。若总共等待了75s后仍未收到响应则返回本错误。

若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接。这是一种硬错误,客户一收到RST就马上返回ECONNREFUSED错误。

若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一个软错误。客户主机内核保存该消息,并按第一种情况所述的时间间隔继续发送SYN。若在某个规定的时间内仍无响应,则把保存的信息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。以下两种情况是有可能的:一是按照本地系统的转发表,根本没有到达远程系统的路径;二是connect调用根本不等待就返回。

9. read

通过连接套接字读取数据

int read(int fd,char *buf,int len);
input:
    fd-连接套接字描述符
    buf-接收数据缓冲区,字节数组
    len-要读取数据大小,字节为单位
output:
    返回读取到的数据字节数。系统接收缓冲区中的数据大于参数len时返回len,缓冲区中的数据小于参数len时返回实	   际长度。失败返回-1

注:接收缓冲区中没有数据时read函数阻塞,出现下列情况时返回

收到数据

连接被关闭/数据被读完了,返回0

连接被复位,返回错误

阻塞过程中收到中断信号,errno=EINTR

10. write

通过连接套接字向对方机器写数据

int write(int fd,char *buf,int len);
input:
    fd-连接socket描述符
    buf-发送数据缓冲区,字节数组
    len-要发送数据大小,字节为单位
output:
    写入的实际字节数。

注:发送缓冲区中空间小于参数len时write函数阻塞 。以下情况函数返回:

发送缓冲区中空间大于参数len

连接被复位,返回错误

阻塞过程中收到中断信号,返回EINTR

11. close

关闭socket

int close(int sockfd);
input:
    sockfd-socket描述符
output:
    0-成功,-1-失败

调用close只是将对sockfd的引用减1(类似于OS中的基于索引节点的文件共享),直到对sockfd的引用为0时才清除sockfd ,TCP协议将继续使用sockfd,直到所有数据发送完成 。

code1

客户机发送256个字符,服务器将这256个字符颠倒顺序然后重新发送给client

server:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <time.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define SERVER_PORT 8086
#define BACKLOG 5


//业务函数,接受client的256个char数据
void serv_respon(int sockfd)
{
	int i;
	int nbytes;
	char buf_recv[1024];
	char buf_send[1024];

	//nbytes=read_all(sockfd,buf_recv,256);

    //读数据到缓冲区
	nbytes = read(sockfd, buf_recv, 256);
	if (nbytes == 0)
		return;
	else if (nbytes < 0)
	{
		fprintf(stderr, "Write error");
		exit(1);
	}

	char *pc_recv = buf_recv + 255;
	char *pc_send = buf_send;

	printf("The data will be return...\n");
	for (i = 0; i < 256; i++)
	{
		*pc_send = *pc_recv;
		printf("%d  ", *pc_send);

		pc_send++;
		pc_recv--;
	}

	printf("\n");

	//nbytes=write_all(sockfd,buf_send,256);
	nbytes = write(sockfd, buf_send, 256);
	if (nbytes < 0)
	{
		fprintf(stderr, "Write error");
		exit(1);
	}
}

int main(int argc, char *argv[])
{
    //听套接字描述符,连接套接字描述符
	int listenfd, connfd;
    //地址struct
	struct sockaddr_in servaddr;
	
    //创建听套接字
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (listenfd < 0)
	{
		fprintf(stderr, "Socket error");
		exit(1);
	}
    
	//setsockopt允许在bind()过程中本地地址可重复使用
	int on = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	printf("reuse addr\n");
	
    //设置地址struct,要注意IP和port要从HBO转换为NBO
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;//设置协议簇
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY本机所有ip
	servaddr.sin_port = htons(SERVER_PORT);//设置端口号
    
    //听套接字与本机ip和端口绑定,这里地址结构体的指针要进行强制类型转换
	if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
	{
		fprintf(stderr, "Bind error");
		exit(1);
	}
	
    //命令听套接字用于监听本地ip和端口,最大连接数为5
	if (listen(listenfd, BACKLOG) < 0)
	{
		fprintf(stderr, "Listen error");
		exit(1);
	}

	printf("listenfd is %d\n", listenfd);
	printf("Listening...\n");

    //死循环,接受数据
	while(1)
	{
        //创建连接套接字,完成与一个client的连接请求
		connfd = accept(listenfd, NULL, NULL);
		if (connfd < 0)
		{
			fprintf(stderr, "Accept error");
			exit(1);
		}

		printf("A SYN requirement is accepted.\n");
		printf("connfd is %d\n", connfd);
		
        //调用业务函数,
		serv_respon(connfd);
		printf("One service finished.\n");
		
        //关闭该连接
		close(connfd);
	}

	close(listenfd);
}

client :

注,client无需创建听套接字

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <time.h>
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include<iostream>
#include<string>

using namespace std;

#define SERVER_PORT 8086

void cli_requ(int sockfd)
{
    char buf[1024];
    int i, n, nbytes;

    printf("The data will be sent...\n");
    srand((int)time(NULL));
    for (i = 0; i < 256; i++)
    {
        buf[i] = random() % 256;
        printf("%d  ", buf[i]);
    }

    printf("\n");
    //nbytes=write_all(sockfd,buf,256);
    //向server发送数据
    nbytes = write(sockfd, buf, 256);
    if (nbytes < 0)
    {
        fprintf(stderr, "Write error");
        exit(1);
    }

    //nbytes=read_all(sockfd,buf,512);
    nbytes = read(sockfd, buf, 512);
    if (nbytes < 0 || nbytes == 0)
    {
        fprintf(stderr, "Read error");
        exit(1);
    }

    sleep(5);
    printf("The received data is...\n");

    for (i = 0; i < nbytes; i++)
        printf("%d  ", buf[i]);

    printf("\n");
}

int main(int argc, char *argv[])
{
    //连接套接字
    int sockfd;
    struct sockaddr_in servaddr;

    //目标IP
    string ip="127.0.0.1";

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if (sockfd < 0)
    {
        fprintf(stderr, "Socket error");
        exit(1);
    }

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVER_PORT);

    if (inet_aton(ip.c_str(), &servaddr.sin_addr) == -1)
    {
        fprintf(stderr, "Inet_aton error");
        exit(1);
    }


    printf("Connecting...\n");

    //使用连接套接字完成与server的连接
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        fprintf(stderr, "Connect error");
        exit(1);
    }

    printf("Connected.\n");

    sleep(3);
    //调用业务函数
    cli_requ(sockfd);

    close(sockfd);
}

启动时先启动server,再启动client

code2

服务器输出客户机的IP与端口号:

server:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXDATASIZE 128
#define BACKLOG 5

int main(int argc,char **argv){
	int sockfd,new_fd,sin_size;
	int port,delay;
	char buf[MAXDATASIZE];
	struct sockaddr_in srvaddr,clientaddr;
	
	if(argc!=3){
		printf("usage:./echoserver port delay\n");
		return 1;
	}
	port=atoi(argv[1]);
	delay=atoi(argv[2]);
	//1.创建网络端点
	sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(sockfd==-1){
		printf("can't create socket\n");
		exit(1);
	}
	//填充地址
	bzero(&srvaddr,sizeof(srvaddr));
	srvaddr.sin_family=AF_INET;
	srvaddr.sin_port=htons(port);
	srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);
	//2.绑定服务器地址和端口
	if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
		printf("bind error\n");
		exit(1);
	}
	//3. 监听端口
	if(listen(sockfd,BACKLOG)==-1){
		printf("listen error\n");
		exit(1);
	}
	for(;;){
		//4.接受客户端连接
		sin_size=sizeof(struct sockaddr_in);
		if((new_fd=accept(sockfd,(struct sockaddr *)&clientaddr,(socklen_t*)&sin_size))==-1){
			printf("accept error\n");
			continue;
		}
		printf("client addr:%s %d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
		//延时
		sleep(delay);
		//6.回送响应
		sprintf(buf,"welcome!");
		//关闭socket
		close(new_fd);
	}
	close(sockfd);
	
	return 0;
}

client

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include<string>
using namespace std;


int main(void){
	int sockfd;
	struct sockaddr_in addr;
	string ip="127.0.0.1";
	sockfd=socket(AF_INET,SOCK_STREAM,0);
	unsigned short port=3000;

	if (sockfd < 0)
    {
        fprintf(stderr, "Socket error");
        exit(1);
    }

    bzero(&addr,sizeof(struct sockaddr_in));
    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    if (inet_aton(ip.c_str(), &addr.sin_addr) == -1)
    {
        fprintf(stderr, "Inet_aton error");
        exit(1);
    }

     printf("Connecting...\n");

    //使用连接套接字完成与server的连接
    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0)
    {
        fprintf(stderr, "Connect error\n");
        exit(1);
    }
    printf("Connected.\n");
    close(sockfd);
}

server启动方法:由于使用了完整的main函数参数,所以需要这样调用:

root@zhg-virtual-machine:/home/zhg/文档/net_program/unit03_DelayServer# ./server 3000 1
client addr:127.0.0.1 37910
client addr:127.0.0.1 37914
client addr:127.0.0.1 37916

补充:argv[0]是./server;argv[1]是3000;argv[2]是1

ubuntu下代码调试

使用gdb进行调试,首先编译时要加入-g命令

然后输入gdbr,若有段错误,显示收到 SIGSEGV 信号,通过man 7 signal查看SIGSEGV的信息

unit 03 高级套接字函数

基本函数

1. DNS解析

查询域名对应的IP

struct hostent* gethostbyname(const char *name);

struct hostent{
	char	 h_name;	/*主机正式名称*/
	char	**h_aliases;	/*别名列表,字符串数组,以NULL结束*/
	int 	h_addrtype;	/*主机地址类型:AF_INET*/
	int 	h_length;	/*主机地址长度:4字节32位*/
	char 	**h_addr_list;	/*主机网络地址列表,多个字节数组,以NULL结束*/
}
#define 	h_addr 	h_addr_list[0]; //主机的第一个网络地址

示例:查询百度的所有ip

#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<sys/socket.h>
#include<netinet/in.h>
#include <netdb.h>
#include<arpa/inet.h>
#include<signal.h>
#include<time.h>
#include <errno.h>
#include <unistd.h>

using namespace std;


int main(void){
	struct hostent* he=gethostbyname("baidu.com");
	if(he!=NULL){
		cout<<"hostname:"<<he->h_name<<endl;
		cout<<"host aliases:"<<endl;
		for(int i=0;he->h_aliases[i]!=NULL;i++){
			cout<<he->h_aliases[i]<<endl;
		}
        
		cout<<"host address:"<<endl;
		for(int i=0;he->h_addr_list[i]!=NULL;i++){
            //注意:由于使用的是网络地址,即无符号long型,所以需要进行转换,将网络地址转换为字符串地址
			struct in_addr*  addr=(struct in_addr*)he->h_addr_list[i];
			cout<<inet_ntoa(*addr)<<endl;
		}
	}
	else{
		cout<<"network error"<<endl;
	}
}


//结果:
root@zhg-virtual-machine:/home/zhg/文档/net_program/unit03_DNS# ./DNSsolve
hostname:baidu.com
host aliases:
host address:
220.181.38.251
220.181.38.148

注:对同一DNS服务器两次调用gethostbyname返回的IP地址列表顺序不同;在不同的DNS服务器上查询,返回结果不同

示例:两种方式获取ip

//若address为字符串地址,直接将其转换为网络地址;否则若为域名,就将其解析为ip的网络地址
int addr_conv(char *address,struct in_addr *inaddr){
	struct hostent *he;
    //参数address为IP字符串地址,直接转换为网络地址
	if(inet_aton(address,inaddr)!=0){
		printf("call inet_aton sucess.\n");
		return 0;
	}
	printf("call inet_aton fail.\n");
    //参数address为域名,需要进行DNS解析
	he=gethostbyname(address);
	if(he!=NULL){
		printf("call gethostbyname sucess.\n");
		show_addr(he);
		*inaddr=*((struct in_addr *)(he->h_addr_list[0]));
		return 0;
	}
	return -1;
}

查询IP对应的域名

struct hostent *gethostbyaddr(const char *addr,size_t len,int family); 
input:
    addr-ip地址,网络地址形式
    len-addr的size
    family-一般为AF_INET

2. recv、send

通过参数控制读写数据,用于流式套接字

int recv(int sockfd,void* buf,int len, int flags);
int send(int sockfd,void* buf,int len,int flags);

input:
    sockfd-连接socket描述符
    buf-发送或接收数据缓冲区
    len-发送或接收数据长度
    flags-发送或接收数据的控制参数
output:
    大于等于0-成功,-1失败

控制参数:

pflags=0,相当于read和write函数

pflags=MSG_DONTROUTE,发送数据不查找路由表,适用于局域网,或同一网段

pflags=MSG_OOB,发送和接收带外数据

pflags=MSG_PEEK,接收数据时不从缓冲区移走数据,其他进程调用read或recv仍然可以读到数据

pflags=MSG_WAITALL,数据量不够时,读操作等待,不返回,但在收到、文件结束符、信号以及出错时,仍然会结束。

标志 recv send
MSG_DONTROUTE
MSG_OOB
MSG_PEEK
MSG_WAITALL

3. shutdown

关闭连接(可选择控制参数)

int shutdown(int sockfd,int howto); 
input:
    sockfd-socket描述符
	howto-控制参数,指定关闭操作的类型
output:
    0-成功,-1失败

控制参数

howto=0,关闭读通道,丢弃尚未读取的数据,对后来接收到的数据返回确认后丢弃

howto=1,关闭写通道,继续发送发送缓冲区未发送完的数据,然后发送FIN字段关闭写通道

phowto=2,关闭读写通道,任何进程不能再操作这个socket

注意点:

shutdown操作连接通道,其他进程不能再使用已被关闭的通道;close操作描述符,其他进程仍然可以使用该socket描述符

close关闭应用程序与socket的接口,调用close之后进程不能再读写这个socket;shutdown可以只关闭一个通道,另一个通道仍然可以操作

4. select

背景:accept、recv、read函数都使用阻塞机制,即若没有数据或连接到来,进程就会阻塞然后由OS挂起休眠,直到有数据到来,OS唤醒进程。

我们可以使用select函数实现非阻塞的连接处理。使用select函数监视socket描述符的变化情况--即可以读写还是异常,从而实现多路复用。如果当前没有client的请求,server等待一定时间后就可以返回然后继续执行后面的代码,避免了阻塞。

int select(int maxfd,
           fd_set *rdset,
           fd_set *wrest,
           fd_set *exset,
           struct timeval *timeout);
input:
    maxfd-需要测试的描述符的最大值,实际测试的描述符从0-maxfd-1  
    rdset-需要测试是否可读的描述符集合(包括处于listen状态的socket接收到accept请求)。若设为NULL值,表示不关心任何读变化
    wrset-需要测试是否可写的描述符集合(包括以非阻塞方式调用connect是否成功)。  
    exset-需要测试是否异常的描述符集合(包括接收带外数据的socket有带外数据到达)   
    timeout-指定测试超时的时间。有三种情况:
    (1)设为NULL,即令select采取阻塞机制,等到集合中某个描述符发生变化为止
    (2)设为0,即纯粹的非阻塞机制,检查完描述符集合后就立即返回
    (3)大于0,即select在这段时间内阻塞,这段时间内有事件到来就立即返回,否则在超时后返回
output:
    有描述符就绪则返回就绪的描述符个数;超时时间内没有描述符就绪返回0;执行失败返回-1


struct df_set : 是一个集合,存放socket描述符
操作df_set的宏定义:
    FD_ZERO(fd_set *fdset)-清空描述符集合 
    FD_SET(int fd,fd_set *fdset)-将一个描述符添加到描述符集合 
    FD_CLR(int fd,fd_set *fdset)-将一个描述符从描述符集合中清除 
    FD_ISSET(int fd,fd_set *fdset)-检测一个描述符是否就绪 
    在设置描述符集合前应该先调用FD_ZERO将集合清空,每次调用select函数前应该重新设置这3个集合 
	三个集合中的描述符可以交叉 


struct timeval{
	long tv_sec;//秒
	long tv_usec;//毫秒
}     

code1

client在三参数时串行工作,四参数时多路复用

server:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#include<iostream>
#include<string>
using namespace std;

#define MAX 128
#define BACKLOG 5
char buffer[MAX]={0};


//参数为server使用的port
int main(int argc,char** argv){
	int lsockfd,wsockfd;
	unsigned short port;
	string ip="127.0.0.1";
	int delay;
	struct sockaddr_in servaddr;

	if(argc!=3){
		cout<<"usage:./server port delay"<<endl;
		return 1;
	}

	port=atoi(argv[1]);
	delay=atoi(argv[2]);

	//1.创建网络端点
	lsockfd=socket(AF_INET,SOCK_STREAM,0);
	if(lsockfd==-1){
		cout<<"can't create socket"<<endl;
		exit(1);
	}
	//填充地址
	bzero(&servaddr,sizeof(servaddr));
	servaddr.sin_family=AF_INET;
	servaddr.sin_port=htons(port);
	if(inet_aton(ip.c_str(),&servaddr.sin_addr)==-1){
		fprintf(stderr, "Inet_aton error");
        exit(1);
	}

	//2.绑定服务器地址和端口
	if(bind(lsockfd,(struct sockaddr *)&servaddr,sizeof(struct sockaddr))==-1){
		printf("bind error\n");
		exit(1);
	}
	//3. 监听端口
	if(listen(lsockfd,BACKLOG)==-1){
		printf("listen error\n");
		exit(1);
	}
	sleep(delay);
	//4.接受客户端连接
	if((wsockfd=accept(lsockfd,NULL,NULL))==-1)
		cout<<"accept error"<<std::endl;

	write(wsockfd,buffer,MAX);

	close(wsockfd);
	close(lsockfd);
}

client:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>

#define MAXDATASIZE 128
#define max(a,b) ((a)>(b)?(a):(b))

int main(int argc, char** argv)
{
	int sockfd1, sockfd2, nbytes;
	char buf[MAXDATASIZE];
	struct sockaddr_in srvaddr1, srvaddr2;
	int port1, port2;
	int multi = 0;
	if (argc < 3) {
		printf("usage:./client port1 port2\n");
		exit(0);
	}
	port1 = atoi(argv[1]);
	port2 = atoi(argv[2]);
	if (argc == 4)
		multi = 1;
	//1.创建网络端点
	sockfd1 = socket(AF_INET, SOCK_STREAM, 0);
	sockfd2 = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd1 == -1 || sockfd2 == -1) {
		printf("create socket error\n");
		exit(1);
	}
	//指定服务器地址(本地socket地址采用默认值)
	bzero(&srvaddr1, sizeof(srvaddr1));
	srvaddr1.sin_family = AF_INET;
	srvaddr1.sin_port = htons(port1);

	if (inet_aton("127.0.0.1", &srvaddr1.sin_addr) == -1) {
		printf("addr convert error\n");
		exit(1);
	}

	memcpy(&srvaddr2, &srvaddr1, sizeof(srvaddr1));
	srvaddr2.sin_port = htons(port2);
	//2.连接服务器
	if (connect(sockfd1, (struct sockaddr*)&srvaddr1, sizeof(struct sockaddr)) == -1
		|| connect(sockfd2, (struct sockaddr*)&srvaddr2, sizeof(struct sockaddr)) == -1) {
		printf("connect error\n");
		exit(1);
	}

	//4.接收响应
	struct timeval starttime, endtime;

	gettimeofday(&starttime, NULL);
	printf("start time:%ld\n", starttime.tv_sec);

	//串行
	if (!multi) {
		//先从服务器1读取数据
		if ((nbytes = read(sockfd1, buf, MAXDATASIZE)) == -1) {
			printf("read error\n");
			exit(1);
		}
		buf[nbytes] = '\0';
		gettimeofday(&endtime, NULL);
		printf("(%ld) server1 respons:%s\n", endtime.tv_sec, buf);
		//再从服务器2读取数据
		if ((nbytes = read(sockfd2, buf, MAXDATASIZE)) == -1) {
			printf("read error\n");
			exit(1);
		}
		buf[nbytes] = '\0';
		gettimeofday(&endtime, NULL);
		printf("(%ld) server2 respons:%s\n", endtime.tv_sec, buf);
	}
	//多路复用
	else {
		int fd1_finished = 0;
		int fd2_finished = 0;
		while (!fd1_finished || !fd2_finished) {
			//监视读请求
			fd_set rdset;
			FD_ZERO(&rdset);
			if (!fd1_finished)
				FD_SET(sockfd1, &rdset);
			if (!fd2_finished)
				FD_SET(sockfd2, &rdset);
			//等待时间为100微秒
			struct timeval tv;	
			tv.tv_sec = 0;
			tv.tv_usec = 100;
			//使用select函数实现多路复用
			int n = select(max(sockfd1, sockfd2) + 1, &rdset, NULL, NULL, &tv);
			if (n <= 0)		//无请求,continue
				continue;
			else {			//有请求
				//检测socket1是否就绪
				if (!fd1_finished && FD_ISSET(sockfd1, &rdset)) {
					if ((nbytes = read(sockfd1, buf, MAXDATASIZE)) == -1) {
						printf("read error\n");
						exit(1);
					}
					buf[nbytes] = '\0';
					gettimeofday(&endtime, NULL);	//返回当前时间
					printf("(%ld) server1 respons:%s\n", endtime.tv_sec, buf);
					fd1_finished = 1;	//结束对服务器1的连接
				}
				//检测socket2是否就绪
				if (!fd2_finished && FD_ISSET(sockfd2, &rdset)) {
					if ((nbytes = read(sockfd2, buf, MAXDATASIZE)) == -1) {
						printf("read error\n");
						exit(1);
					}
					buf[nbytes] = '\0';
					gettimeofday(&endtime, NULL);
					printf("(%ld) server2 respons:%s\n", endtime.tv_sec, buf);
					fd2_finished = 1;
				}
			}
		}
	}
	//关闭socket
	close(sockfd1);
	close(sockfd2);
	return 0;
}

验证方法:

首先依次运行程序:

./server 3000 20
./server 3001 2
zhg@zhg-virtual-machine:~/文档/net_program/unit03_MultiClient$ ./client 3000 3001
start time:1652787769
(1652787778) server1 respons:
(1652787778) server2 respons:

即使server2早早可以向client发送数据,但是由于client被read函数阻塞,所以只能等server1休眠20秒并被服务后才轮到server2服务。

然后这样运行程序:

./server 3000 20
./server 3001 2
zhg@zhg-virtual-machine:~/文档/net_program/unit03_MultiClient$ ./client 3000 3001 1
start time:1652788858
(1652788858) server2 respons:
(1652788858) server1 respons:

此时,client使用select函数监控两个socket,由于server2结束休眠更早,所以client先从server2接收数据,再从server1接收数据。

unit 04 数据报式套接字与原始socket编程

UDP协议是无连接的,所以不需要listen监听端口和accept接受连接

流程图

image

UDP服务器特点

服务器不接受客户端连接,只需监听端口

循环服务器,可以交替处理各个客户端数据包,不会被一个客户端独占

客户端不用建立连接,第一次调用sendto函数时,UDP协议为这个UDP socket选择一个端口号,以后的发送和接收操作均使用这个端口号

客户端可以接收来自任何主机的数据报

客户端可能永远阻塞(当服务器主机崩溃时) 因为无连接

nUDP协议不保证数据报可靠到达,不保证数据报顺序到达,没有流量控制

原始socket编程

原始socket直接针对IP数据包编程,具有更强的灵活性,能够访问ICMP和IGMP数据包,可以编写基于IP协议的高层协议

基本函数

1. recvfrom

接收UDP数据报

int recvfrom(int sockfd,
             void *buf,
             int len,
             unsigned char flags,
             struct socketaddr *from,
             socklen_t *addrlen); 

input:
    sockfd-连接socket描述符
    buf-发送或接收数据缓冲区
    len-发送或接收数据长度
    flags-发送或接收数据的控制参数,一般设为0
    from-发送者socket地址,有两种特殊情况:(1)NULL表示不需要 (2)若是一个只被初始化了的结构体,则函数结束后会将数据发送方的地址储存在from中
	addrlen-socket地址结构体的size,from为NULL时必须置为NULL,若from只被初始化,那么addrlen也是一个只被初始化了的变量的指针
output:
    收到的字节数。大于等于0-成功,-1失败

注:

UDP协议给每个UDP SOCKET设置一个接收缓冲区,每一个收到的数据报根据其端口放在不同缓冲区。

recvfrom函数每次从接收缓冲区队列取回一个数据报,没有数据报时将阻塞,返回值为0表示收到长度为0的空数据报,不表示对方已结束发送

2. sendto

发送UDP数据报

int sendto(int sockfd,
           const void *buf,
           int len,
           unsigned char flags,
           struct socketaddr *to,
           int  tolen); 
input:
    前4个参数和send相同,len宜<548
	to-接收者socket地址
	addrlen-socket地址结构体的size
output:
    发送的字节数。>=0成功,-1失败 

注:

每次调用sendto都必须指明接收方socket地址,UDP协议没有设置发送缓冲区,sendto将数据报拷贝到系统缓冲区后返回,通常不会阻塞

3. 有连接的UDP

在udp socket上调用connect函数,不会产生3次握手过程,只记录连接另一方的IP和端口,connect函数立即返回。此后,发送UDP数据报时不用指定服务器地址,且只能接收来自指定服务器的数据报

pudp socket允许对一个socket多次调用connect函数,每次调用connect函数将释放原来绑定的地址,绑定到新地址

4. 创建原始socket

int socket (int family, int type, int protocol);
input:
	family-AF_INET
	type-SOCK_RAW
	protocol:
		IPPROTO_ICMP-ICMP数据包
        IPPROTO_IGMP-IGMP数据包
        IPPROTO_IP-IP数据包
output:
	返回套接字描述符。错误的话返回-1,系统全局变量errno为错误代码

5. 设置IP选项

设置是否自动填充IP报文的首部

int on=1;
setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on));
on=0,由协议自动填充
on=1,用户程序填充

6. 绑定本机IP

绑定本机IP:

使用bind函数绑定本地IP地址,发送的IP数据包的源地址就是bind绑定的地址;不调用bind函数时将以主网络接口IP地址为源地址

如果设置了IP选项IP_HDRINCL,bind函数将不起作用,必须手工填充每个IP数据包的源地址

绑定对方IP:

使用connect函数绑定对方地址,发送的IP数据包目的地址就是connect绑定的对方地址

用connect绑定对方地址后可以使用函数write和send发送IP数据包

若不调用connect函数,每次发送IP数据包必须使用sendto函数指定对方IP地址

没有设置IP_HDRINCL选项时只能填充IP数据包的数据区;设置了IP_HDRINCL选项后可以填充IP数据包首部和数据区

7. 原始socket接受过程

image

8. ICMP数据包格式

类型:8-echo request,0-echo response

校验和:采用IP校验和计算方法

标识符:进程号

序列号:从0开始,依次加1

image

数据结构:

struct icmphdr{
	__u8	type;
	__u8	code;
	__u16	checksum;
	union{
		struct{
			__u16	id;
			__u16	sequence;
		}echo;
		struct{
			__u16	__unused;
			__u16	mtu;
		}frag;
	}un;
};

code1 udp服务器、客户端

server:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <sys/time.h>

using namespace std;

int main(int argc, char** argv)
{
	if (argc != 2)
	{
		cout << "usage: ./server port" << endl;
		return 1;
	}
	short port = atoi(argv[1]);

    //创建数据报式套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd == -1)
	{
		cout << "create socket error" << endl;
		return 1;
	}
    //创建地址结构体
	sockaddr_in addr;
	bzero(&addr, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = htonl(INADDR_ANY);
	//绑定服务器地址
	if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
	{
		cout << "bind error" << endl;
		return 1;
	}
    
    //接收数据
	for (;;)
	{
		char 		buf[32];
		sockaddr_in 	client_addr;	//client的地址结构体
		socklen_t		addr_len;
		//接收客户端数据包,n为接收到的数据的字节数
		int n = recvfrom(sockfd,
			buf,
			16,		//一次接受16字节数据
			0,		//flag参数为0
			(struct sockaddr*)&client_addr,		//使用只被初始化的地址结构体,函数完成后会自动将对方的地址填充到client_addr中
			&addr_len);
		if (n >= 0)
		{
			buf[n] = 0;
			cout << "recv:" << buf << endl;
			struct timeval tv;
			gettimeofday(&tv, NULL);
			sprintf(buf, "%d %d", (int)tv.tv_sec, (int)tv.tv_usec);
			//利用recvfron中得到的地址回送数据包
			sendto(sockfd,
				buf,
				strlen(buf),	//注:之前收到的数据不一定为16个字节,所以要用strlen
				0,	
				(struct sockaddr*)&client_addr,
				sizeof(client_addr));
		}
	}
	close(sockfd);
	return 0;
}

client:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>

using namespace std;

int main(int argc, char** argv)
{
	if (argc < 2)
	{
		cout << "usage:./client port" << endl;
		return 1;
	}
	short port = atoi(argv[1]);

	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd == -1)
	{
		cout << "create socket error" << endl;
		return 1;
	}

    //记录服务器的地址
	sockaddr_in addr;
	bzero(&addr, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = htonl(INADDR_ANY);
	if (argc == 3 && strcmp(argv[2], "-c") == 0)
	{
		//使用有连接的UDP,记录服务器地址
		connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
	}
	for (int i = 0; i < 10; i++)
	{
		char buf[16];
		sprintf(buf, "%d hello", getpid());
		cout << "send:" << buf << endl;
		int n;
		if (argc == 3 && strcmp(argv[2], "-c") == 0)
		{
			//之前connect过,所以发送时不需要服务器地址
			n = sendto(sockfd, buf, strlen(buf), 0, NULL, 0);
		}
		else
		{
			//发送时需要服务器地址
			n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&addr, sizeof(addr));
		}
		n = recvfrom(sockfd, buf, 16, 0, NULL, NULL);
		if (n >= 0)
		{
			buf[n] = 0;
			cout << "recv:" << buf << endl;
		}
		sleep(1);
	}
	close(sockfd);
	return 0;
}

unit 05 Linux进程控制

Linux创建进程的机制

首先:进程==PCB+代码+数据

一个进程调用fork函数后,系统给新的进程分配资源,然后将父进程的代码和数据copy到子进程中。

当父进程fork后,假设下一条指令为\(I_k\),那么子进程中下一条指令也是\(I_k\)

fork有三种返回值:

(1)父进程中,fork返回子进程的pid

(2)子进程中,fork返回0(虽然子进程从\(I_k\)开始执行,但是fork的返回值并非是从父进程中copy的,而是设置为0)

(3)出错,返回1个负数

Linux信号机制

1. 定义

linux系统为进程正常执行过程中可能发生的各种软、硬件状态定义了一组信号,异步传送给进程。主要目的是解决共享资源的使用和保护,用于通知进程发生了异步事件。

信号是对中断的软件模拟。

信号是异步的,信号何时到达是未知的

信号来源:硬件-键盘或硬件错误;软件-其他进程或内核

2. 信号分类

SIGALARM-计时器到时

SIGCHLD-子进程停止或退出时通知父进程

SIGKILL-终止进程

SIGSTOP-停止进程

SIGINT-中断字符,CTRL+C

3. 接收信号

信号到来时,不管进程是休眠还是运行,都需要采取中断立即执行信号处理函数。若在sleep中信号到来,则处理信号后不再继续sleep

进程对信号的响应 :

执行缺省操作

忽略信号(SIGKILL和SIGSTOP不能忽略

用户捕获信号,执行用户的信号处理函数

信号是一次性的,不会多次重传。进程用一个队列储存收到的信号?

4. 处理信号

使用sigaction函数

5. 临界区与进程同步

为了避免临界区代码被中断,可以在执行临界区代码时用sigprocmask屏蔽某些信号

Sigsuspend可用于等待某个信号实现进程同步

进程组

进程组就是一系列相互关联的进程集合,系统中的每一个进程也必须从属于某一个进程组;每个进程组中都会有一个唯一的 ID(process group id),简称 PGID;PGID 一般等同于进程组的创建进程的 Process ID,而这个进程一般也会被称为进程组先导(process group leader),同一进程组中除了进程组先导外的其他进程都是其子进程;

fork的子进程默认跟父进程是一个进程组的

僵尸进程

1. 定义

子进程终止时,会向父进程发送一个SIGCHLD信号。父进程默认会忽略该信号而不处理

如果父进程存在且未处理SIGCHLD信号则子进程变为僵尸进程,僵尸进程占据系统进程表项

通常情况下,子进程退出后,父进程会使用 waitwaitpid 函数进行回收子进程的资源,并获得子进程的终止状态。

但是,如果父进程先于子进程结束,则子进程成为孤儿进程。孤儿进程将被 init 进程(进程号为1)领养,并由 init 进程对孤儿进程完成状态收集工作。

而如果子进程先于父进程退出,同时父进程没有用wait处理信号,信号就被忽略,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。当父进程自动或强制结束后,子进程的资源会被自动回收。

注意:普通进程可以被 kill ,但僵尸进程是不行的。

2. 清理

如何清理僵尸进程

(1) 方法1

忽略SIGCHLD信号(信号处理函数为SIG_IGN)。忽略SIGCHLD信号时,系统将清除子进程的进程表项,这种方法依赖于Linux版本的实现。

看下面的代码:

#include <sys/types.h>
#include <unistd.h>
#include<cstdio>
#include<iostream>
using namespace std;

int main(void){
	pid_t pid=fork();
	if(pid==0){
		cout<<"child process"<<endl;
		sleep(1);
	}
	else{
		while(true)		;	
	}
}

运行代码后,使用top指令查看进程情况,可以看到第二行的1 zombie,说明子进程变成了僵尸进程:

image

然后杀死僵尸进程的父进程。原理:父进程结束后,子进程的资源会被自动回收。

使用命令查找僵尸进程:其中egrep '^[Zz]'是正则表达式,表示名字为Z开头的进程(即僵尸进程)

ps -e -o stat,ppid,pid,cmd | egrep '^[Zz]'

结果:僵尸进程的pid为7679,ppid为7678

image

杀死父进程:

kill -9 7678

之后再次调用top,可以发现僵尸进程消失了。

(2) 方法2

理论上要杀死父进程,但如果僵尸进程的父进程为init,那么就不能杀死。

我们采用挂起僵尸进程的方法

kill -HUP 

然而,这么干不是真正的清除僵尸进程,而是将其挂起等效于让其什么都不干,使用top再看发现僵尸进程还是存在。

(3) 方法3

调用函数waitwaitpid等待子进程

当子进程终止时,内核就会向它的父进程发送一个SIGCHLD信号,父进程可以选择忽略该信号,也可以提供一个接收到信号以后的处理函数。对于这种信号的系统默认动作是忽略它。

在父进程接收到SIGCHLD信号后就应该调用 wait 或 waitpid 函数对子进程进行善后处理,处理SIGCHLD信号,释放子进程占用的资源。

(4) 方法4

捕获SIGCHLD信号。如果多个SIGCHLD信号同时到达,进程将只收到一个,因此信号处理函数中必须循环调用waitpid处理多个子进程终止。waitpid函数要设置选项WNOHANG防止阻塞。(其实是可以用循环wait的,需要判断errno类型,见(34条消息) wait如何处理多进程(多个子进程)_红莲之殇的博客-CSDN博客_父进程wait多个子进程)

(5) 方法5

调用fork两次,使子进程成为孤儿进程,有init进程管理然后自动回收资源。

见链接:fork两次 避免僵尸进程 - tangr206 - 博客园 (cnblogs.com)

核心思想:第一次fork出的子进程fork出新的子进程后就调用exit退出,然后新的子进程就被init进程收养了。当然第一次fork出的子进程会成为僵尸进程。

守护进程

1. 定义

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的

守护进程特点:

生存期为系统执行时间

一直等待事件发生并处理事件

可以利用其他进程完成事件处理

不和任何终端发生联系

2. 用户进程转换为守护进程

教程链接:搞懂进程组、会话、控制终端关系,才能明白守护进程如何创建 - 知乎 (zhihu.com)

步骤:

(1) 调用fork,然后父进程exit退出,子进程pid1成为孤儿进程被init进程收养

(2) 调用setsid创建新的session, 子进程pid1成为头进程。从而脱离控制台

(3) 忽略信号SIGHUP,调用fork,然后命令父进程(头进程pid1)终结

(4) 调用函数chdir(“/”),使进程不使用任何目录 ,从而脱离文件系统

(5) 调用函数unmask(0),使进程对任何写的内容有权限(最大权限)

(6) 关闭所有打开的文件描述符。先要通过函数sysconf()来获取系统最大的文件打开数。

(7) 为标准输入(0),标准输出(1),标准错误输出(2)打开新的文件描述符

(8) 处理信号SIGCLD,避免守护进程的子进程成为僵尸进程

注意:守护进程的

缺点:麻烦死了(然而张彤连着两年考这个,流汗黄豆)

改进:调用darmon库函数直接将当前用户进程转换为守护进程

3. 守护进程服务器

原理图:

image

注意:守护进程关闭连接socket后,子进程仍可以用该socket进行通信。为什么?OS中的文件节点链接法的原因。守护进程关闭了socket,但是socket描述符是个文件,它还被子进程引用,所以没有真正被关闭。

4. inetd超级服务器

上述守护进程服务器需要我们自己来写守护进程和fork,而Linux提供了inetd超级服务器,它相当于上述的守护进程服务器,用户只需要把处理网络请求的子程序加入到配置文件中即可。

inetd是监视一些网络请求的守护进程,其根据网络请求来调用相应的服务进程来处理连接请求。它可以为多种服务管理连接,当 inetd接到连接时,它能够确定连接所需的程序,启动相应的进程,并把 socket交给它服务 socket会作为程序的标准输入、输出和错误输出描述符)。使用 inetd来运行那些负载不重的服务有助于降低系统负载,因为它不需要为每个服务都启动独立的服务程序。

inted服务器充当一个功能就是创建socket服务端的前半段,即创建socket---->bind(端口)---->监听---->accept(接受信号),当来一个此端口的请求,他会fork+exec来执行相对应的服务程序

/etc/inetd.conf则是inetd的配置文件。inetd.conf文件告诉inetd监听哪些网络端口,为每个端口启动哪个服务。

以下是配置文件一行的内容。

服务名(自己定义)  Socket类型  协议  等待状态  服务器子程序的路径
Socket类型:stream,dgram,raw
协议:tcp , udp
等待状态:nowait:超级服务器在fork一个子进程后,可以立即在同一个端口接受另一个请求并创建新的子进程。
		wait:需要等待
路径:子程序的路径,需要是可执行文件		

注意:配置文件中使用的是服务名,所以会在/etc/service文件中找服务所对应的端口。所以要提前在该文件中加入服务的端口配置信息

服务名  port号

使用#注释,更新配置文件后,要向inetd进程发送一个SIGHUP信号来重新加载配置文件。

killall -HUP inetd

虚拟机使用inetd超级服务器前需要先安装该服务。

关于如何将连接套接字从inetd服务器传递给子进程:https://hmgle.github.io/unix/linux/inetd/2014/10/19/inetd-interface.htmlhttps://blog.csdn.net/WeinKee/article/details/106864937

通俗的说,就是延续了unix系统的传统,inetd守护进程将连接socket描述符用DUP2函数重定向STDIN_FILENO, STDOUT_FILENOSTDERR_FILENO(重定向就是OS中的基于索引的文件共享,多个文件描述符指向同一个文件)。子进程从标准输入读,相当于从所处理的套接字读;子进程往标准输出或标准错误上写,相当于往所处理套接字写。

5. 判断守护进程

使用以下命令查看所有没有控制终端的进程

ps axj [|grep [进程名]]

所有的守护进程都是以超级用户启动的(UID为0); 没有控制终端(TTY为?); 终端进程组ID为-1(TPGID表示终端进程组ID,该值表示与控制终端相关的前台进程组,如果未和任何终端相关,其值为-1);

关于守护进程的父进程,要注意:

历史上,Linux 的启动一直采用init进程;下面的命令用来启动服务。

这种方法有两个缺点:

(1) 启动时间长。init进程是串行启动,只有前一个进程启动完,才会启动下一个进程。

(2) 启动脚本复杂。init进程只是执行启动脚本,不管其他事情。脚本需要自己处理各种情况,这往往使得脚本变得很长。

但现在Linux系统使用的是Systemd 进程作为守护进程的父进程。它的设计目标是,为系统的启动和管理提供一套完整的解决方案。根据 Linux 惯例,字母d是守护进程(daemon)的缩写。 Systemd 这个名字的含义,就是它要守护整个系统。(教材太老了,网上的教程也很老,没有跟上技术的发展)

基本函数

1. fork

创建一个进程,调用者成为父进程,新进程为子进程

pid_t fork(void);
input:void
output:
    >0,子进程的进程id,只在父进程中返回
    -1,调用失败
    =0,只在子进程返回

注:

子进程与父进程共享打开的文件描述符,所以socket描述符也是共享的

2. kill

注:这个函数不是杀进程的

该系统调用可以用来向任何进程或进程组发送任何信号

int kill(pid_t pid,int sig);
input:
    pid:接收信号的进程或进程组
    sig:要发送的信号
output:
	执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno
参数pid的值 接收信号的进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 && pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程

3.raise

向进程自身发送信号

int raise(int sig);
input:
    要发送的信号
output:
    成功返回 0;否则,返回 -1

4. alarm

在指定的时间后,将向进程自身发送SIGALRM信号

unsigned int alarm(unsigned int seconds);
input:
    等待的时间(秒为单位)。后一次设定将取消前一次的设定。
output:
    返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。

5. abort

向进程自身发送SIGABORT信号,默认情况下进程会异常退出,但可定义自己的信号处理函数

void abort();

6. sigqueue

sigqueue()是比较新的发送信号系统调用,支持信号带有参数,与函数sigaction()配合使用。

它能传递更多的附加信息,只能向单个进程(不能向进程组)发送信号

int sigqueue(pid_t pid, int sig, const union sigval val);
output:
    成功返回 0;否则,返回 -1。

typedef union sigval {
               int  sival_int;
               void *sival_ptr;
}sigval_t;

7. sigaction

进程处理接收到的信号

int sigaction(int signum, 
			  const struct sigaction *act,
			  struct sigaction *oldact); 
input:
    signum—指定需要捕获的信号,SIGKILL和SIGSTOP不能指定
    act—指定处理捕获信号的新动作
    oldact—存储旧的动作


struct sigaction {                  
	void (*sa_handler)(int);  			   //函数指针,名为sa_handler
	void (*sa_sigaction)(int, siginfo_t *, void *); //函数指针,名为sa_sigaction
	sigset_t sa_mask; 		//屏蔽的信号集
	int sa_flags;			//标志,SA_SIGINFO
	void (*sa_restorer)(void); 	//已废弃
}    

sigaction结构体成员:

(1) sa_handler/sa_sigaction-信号处理函数。

使用默认动作时设置为SIG_DFL;忽略信号时设置为SIG_IGN

使用用户指定的处理函数时设置为相应处理函数

sa_flags=SA_SIGINFO时sa_sigaction有效

(2) sa_mask-指定信号处理函数中被屏蔽的信号集,通常被处理的信号本身被屏蔽

(3) sa_flags-影响信号处理函数行为的标志

SA_SIGINFO时sa_sigaction有效

SA_ONESHOT或SA_RESETHAND-信号处理函数调用后,将信号的动作改回默认动作

SA_RESTART-使某些系统调用在被信号中断后能自动重新执行

SA_NOCLDSTOP-当signum=SIGCHLD时,子进程停止不通知父进程

SA_NOMASK或SA_NODEFER-在某个信号的处理过程中,这个信号不被屏蔽

信号处理注意点:

一个信号处理器一旦被设置将一直起作用,除非在设置时使用了SA_ONESHOT标志

一个信号处理器执行过程中新到达的同一信号将被屏蔽。另外,可以在参数sa_mask中指定需要屏蔽的其它信号

一个信号被屏蔽时多次产生这个信号,当解除屏蔽时,这个信号只被发送一次

为了避免临界区代码被中断,可以在执行临界区代码时用sigprocmask屏蔽某些信号

Sigsuspend可用于等待某个信号实现进程同步

8. 信号集处理函数

int sigemptyset(sigset_t *set);
功能:清空信号集set 
int sigfillset(sigset_t *set); 
功能:填满信号集set 
int sigaddset(sigset_t *set,int signum); 
功能:在信号集set中添加一个信号signum
sigdelset(sigset_t *set,int signum); 
功能:从信号集set中删除一个信号signum
int sigismember(const sigset_t *set,int signum); 
功能:测试信号signum是否属于信号集set

9. pause

导致进程睡眠,只有非忽略信号( 捕获信号或者终止信号)才会唤醒

10. exit

进程退出

void exit(int status);

细节:

如果进程是某一控制终端的进程组组长,则向这个进程组的所有进程发送信号SIGHUP(然后同进程组其他进程也会结束)

关闭进程打开的所有文件描述符

如果进程有子进程,则将这些子进程的父进程设置为init

父进程发送信号SIGCHLD

11. wait, waitpid

pid_t wait(int *status); 
父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出。如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
input:
    status - 一般设置为NULL
output:
    成功,wait会返回被收集的子进程的进程ID;如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD
pid_t waitpid(pid_t pid,int *status,int option);
input:
    pid>0,只等待进程id等于pid的子进程退出;pid=-1,等待任何一个子进程退出,同wait
	option-选项,WNOHANG-无子进程退出时不阻塞,即函数不会等待子进程退出发来信号,而是直接返回0
	status:储存状态信息,一般设置为NULL
output:
    当正常返回的时候,waitpid返回收集到的子进程的进程ID;
	如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回-1,这时errno被设置为ECHILD;

12. daemon

将当前进程转换为一个守护进程。其内部实现就是之前所说的用户进程转为守护进程的那一套,只是用函数封装起来而已。

int daemon ( int __nochdir, int __noclose);
input:
	__nochdir的值为0,则将切换工作目录为根目录
    __noclose为0,则将标准输入,输出和标准错误都重定向到/dev /null
output:
	成功返回0,失败返回-1
        
DESCRIPTION         
       The daemon() function is for programs wishing to detach themselves
       from the controlling terminal and run in the background as system
       daemons.
       If nochdir is zero, daemon() changes the process's current working
       directory to the root directory ("/"); otherwise, the current working
       directory is left unchanged.
 
 
       If noclose is zero, daemon() redirects standard input, standard
       output and standard error to /dev/null; otherwise, no changes are
       made to these file descriptors.
 
RETURN VALUE         
       (This function forks, and if the fork(2) succeeds, the parent calls
       _exit(2), so that further errors are seen by the child only.)  On
       success daemon() returns zero.  If an error occurs, daemon() returns
       -1 and sets errno to any of the errors specified for the fork(2) and
       setsid(2). 

简单的例程:

#include<unistd.h>
#include<cstdio>
#include<iostream>

using namespace std;


int main(void){
    //这里还是用户进程
	cout<<"before"<<endl;
	if(daemon(0,0)<0){
		cout<<"fuck u error"<<endl;
		exit(0);
	}

	//从这里开始转换为守护进程		
	while(true)	{
		sleep(1);
	}
	return 0;
}

补充:linux命令查看进程情况:

ps -ef |grep [进程名,可使用正规表达式]
结果依次为
UID: 说明该程序被谁拥有
PID:就是指该程序的 ID
PPID: 就是指该程序父级程序的 ID
C: 指的是 CPU 使用的百分比
STIME: 程序的启动时间
TTY: 指的是登录终端
TIME : 指程序使用掉 CPU 的时间
CMD: 下达的指令

判定进程是否为守护进程:

root@zhg-virtual-machine:/home/zhg/文档/net_program/unit05_daemonserver# ps ajx|grep test
     1   1407   1407   1407 ?            -1 Ss     113   0:00 /usr/sbin/kerneloops --test
  2061   3402   3400   2725 pts/0      5005 Sl       0   0:07 subl --detached test.cpp
  2061   4455   4455   4455 ?            -1 Ss       0   0:00 ./test
  2061   4647   4647   4647 ?            -1 Ss       0   0:00 ./test
  4139   5006   5005   2725 pts/0      5005 S+       0   0:00 grep --color=auto test

可看到test进程的TPGID=-1(没有与任何终端相关),TTY=?(没有控制终端),UID=0(以超级用户启动),可知其为守护进程。再看一下它的父进程:

root@zhg-virtual-machine:/home/zhg/文档/net_program/unit05_daemonserver# ps 2061
   PID TTY      STAT   TIME COMMAND
  2061 ?        Ss     0:00 /lib/systemd/systemd --user

注意:其父进程是systemd,即新的技术标准,而不是旧的init进程。

code1 创建子进程

创建一个子进程

#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include<stdio.h> 
#include<stdlib.h> 
#include<errno.h> 

using namespace std;

int main(int argc,char **argv)
{
	pid_t child_pid=fork();
	if(child_pid==0){
		//子进程程序
		for(int i=0;i<5;i++){
			//getppid:获取父进程的pid
			cout<<"child process:ppid="<<getppid()<<",pid="<<getpid()<<endl;
			sleep(1);
		}
		exit(0);
	}
	else if(child_pid>0){
		//父进程程序
		for(int i=0;i<5;i++){
			cout<<"parent process:ppid="<<getppid()<<",pid="<<getpid()<<endl;
			sleep(2);
		}
		exit(0);
	}
	else{
		//调用失败
		cout<<"create child process fail"<<endl;
	}
	return 0;
}

从结果可看出父子进程并发执行

root@zhg-virtual-machine:/home/zhg/文档/net_program/unit05_fork# ./fork
parent process:ppid=2701,pid=2813
child process:ppid=2813,pid=2814
child process:ppid=2813,pid=2814
parent process:ppid=2701,pid=2813
child process:ppid=2813,pid=2814
child process:ppid=2813,pid=2814
child process:ppid=2813,pid=2814
parent process:ppid=2701,pid=2813
parent process:ppid=2701,pid=2813
parent process:ppid=2701,pid=2813

创建5个子进程:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <errno.h>
#include <stdio.h> 
#include <stdlib.h> 

#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h>
#include <string.h>

using namespace std;

//启动方法:./fork2 5
int main(int argc, char** argv)
{
	pid_t pid;
	int n = atoi(argv[1]);

	for (int i = 0; i < n; i++)
	{
		pid = fork();
		if (pid == 0)	//子进程程序
		{
			cout << "child process:ppid=" << getppid() << ",pid=" << getpid() << endl;
			for (int j = 0; j < 3; j++) {
				sleep(1);	//sleep 1秒 
				cout << "I am child " << getpid() << endl;
				cout << "		I have " << 9 - j << "second left" << endl;
			}
			exit(0);
		}
		else if (pid > 0)	//父进程程序
		{
			cout << "parent process:ppid=" << getppid() << ",pid=" << getpid() << endl;
			sleep(1);
		}
		else	//调用失败
			cout << "create child process fail" << endl;
	}

	cout << "parent process exit" << endl;
	return 0;
}

code2 共享文件

父子进程共享一个文件描述符。test.txt中的内容为xwd。

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <iostream>

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

using namespace std;

int		var=0;

int main(int argc,char **argv){
	int 	fd;	//文件描述符
	pid_t 	pid;
	char	pbuf[2];
	
	fd=open("./test.txt",O_RDONLY);
	read(fd,pbuf,1);
	cout<<"before fork pbuf[0]="<<pbuf[0]<<",var="<<var<<endl;
	cout<<"parents fd="<<fd<<endl;
	pid=fork();
	if(pid==0){
		//子进程程序
		char cbuf[2];
		read(fd,cbuf,1);
		var=10;
		cout<<"cbuf[0]="<<cbuf[0]<<",var="<<var<<endl;
		cout<<"child fd="<<fd<<endl;
		
		exit(0);
	}
	else if(pid>0){
		//父进程程序
		wait(NULL);//等待子进程结束
		read(fd,pbuf,1);
		cout<<"after fork pbuf[0]="<<pbuf[0]<<",var="<<var<<endl;
		cout<<"parents fd="<<fd<<endl;
	}
	else{
		//调用失败
		cout<<"create child process fail"<<endl;
	}
	close(fd);
	return 0;
}

父子进程共享一个文件描述符,所以可以顺序读取文件中的内容,结果为:

root@zhg-virtual-machine:/home/zhg/文档/net_program/unit05_share# ./sharefile
before fork pbuf[0]=x,var=0
parents fd=3
cbuf[0]=w,var=10
child fd=3
after fork pbuf[0]=d,var=0
parents fd=3

补充:linux中一切都是文件,socket、管道等都是文件,所以都有对应的文件描述符。

code3 信号处理

每隔1秒进程向自身发送一个SIGALRM信号,把进程从sleep状态中唤醒。

#include "signal.h" 
#include "unistd.h" 

#include <iostream>
#include <stdio.h> 
#include <stdlib.h> 
#include <errno.h> 

using namespace std;

//自定义的信号处理函数
static void my_op(int);

int main()
{
	sigset_t new_mask, old_mask, pending_mask;
	struct sigaction act;

	//处理进程收到的信号
	sigemptyset(&act.sa_mask);	//清空被屏蔽的信号集
	act.sa_flags = SA_SIGINFO; //设此标志后参数才可以传递给信号处理函数
	act.sa_handler = my_op;		//函数句柄(指针)
	if (sigaction(SIGALRM, &act, NULL)) //SIGRTMIN+10
		printf("install signal SIGALRM error\n");

	//cout<<"The pid is "<<getpid()<<endl;
	alarm(1);
	while(true)
		sleep(10);
	cout<<"end"<<endl;
}

static void my_op(int signum)
{
	printf("receive signal %d \n", signum);
	alarm(1);
}

结果:

root@zhg-virtual-machine:/home/zhg/文档/net_program/unit05_signal# ./sigset
receive signal 14 
receive signal 14 
receive signal 14 
receive signal 14 
receive signal 14 
receive signal 14 

code4 inetd超级服务器使用

首先修改/etc/inetd.conf文件,加入该行:

myserver1 stream tcp wait root /home/zhg/文档/net_program/unit05_myserver1/myserver1 myserver1
#root表示使用权限,最后的myserver1就是argv[0],即函数名

然后在/etc/services中增加对服务的说明:监听6324端口(FIFA数字,笑的)

myserver1	6324/tcp

编写服务程序myserver1.cpp:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <iostream>
#include <signal.h>
#include <fcntl.h>
using namespace std;

#define MAX 256

//get message from client
int main(int argc,char** argv){
	char buf[MAX]="I am zhg,fuck u xdu";
	int maxlen=30;

	if(write(STDOUT_FILENO,"I am zhg,fuck u xdu",30)<0)
		exit(0);
	close(STDOUT_FILENO);
}

编写client.cpp测试inetd服务

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <iostream>
#include <signal.h>
using namespace std;

#define MAX 256

int main(void){
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(sockfd<0){
		cout<<"error"<<endl;
		exit(0);
	}

	sockaddr_in srvaddr;
	string ip="127.0.0.1";
	unsigned short port=6324;
	bzero(&srvaddr,sizeof(srvaddr));
	srvaddr.sin_family=AF_INET;
	srvaddr.sin_port=htons(port);
	inet_aton(ip.c_str(),&srvaddr.sin_addr);

	if(connect(sockfd,(sockaddr*)&srvaddr,sizeof(sockaddr))==-1){
		cout<<"connect error"<<endl;
		exit(0);
	}
	cout<<"connected"<<endl;

	char buf[MAX]={0};
	int maxlen=30;
	int len=read(sockfd,buf,30);

	if(len<0){
		cout<<"error"<<endl;
		exit(0);
	}
	buf[len]='\0';
	cout<<buf<<endl;
	close(sockfd);
	return 0;
}

测试结果:

code5 自定义守护进程服务器且提供并发服务

server:

代码的细节写在了注释中。注意;如果是监听多个端口,还需要加入select函数来避免阻塞

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <iostream>
#include <signal.h>
#include <fcntl.h>
using namespace std;

string ip="127.0.0.1";
unsigned short port=6000;
int BACKLOG=10;
const int MAX=256;

int main(int argc,char** argv){
    //调用daemon函数将当前进程转换为守护进程
	if(daemon(0,0)<0){
		cout<<"daemon error"<<endl;
		exit(0);
	}
	//前面都是基础的创建监听套接字那一套
	int lsockfd=socket(AF_INET,SOCK_STREAM,0);
	if(lsockfd<0){
		cout<<"create socket error"<<endl;
		exit(0);
	}

	sockaddr_in srvaddr;
	bzero(&srvaddr,sizeof(srvaddr));
	srvaddr.sin_family=AF_INET;
	srvaddr.sin_port=htons(port);
	inet_aton(ip.c_str(),&srvaddr.sin_addr);

	if(bind(lsockfd,(sockaddr*)&srvaddr,sizeof(sockaddr))<0){
		cout<<"bind error"<<endl;
		exit(0);
	}
	if(listen(lsockfd,BACKLOG)<0){
		cout<<"listen error"<<endl;
		exit(0);
	}
    
	while(true){
        //创建连接套接字
		int csockfd=accept(lsockfd,NULL,NULL);
		if(csockfd<0){
			cout<<"accept error"<<endl;
			continue;
		}
		
        //创建子进程
		int pid=fork();
		if(pid==0){
			char buf[MAX]="I am zhg,fuck u xdu";
			int len=write(csockfd,buf,30);
			close(csockfd);
            exit(0);	//子进程服务完后需要终结进程,否则会开始执行循环创建更多的子进程
		}
		else if(pid>0){
			close(csockfd);		//守护进程要关闭套接字描述符。注意:关了以后子进程照样可以使用描述符
            //使用waitpid函数非阻塞的完成清除僵尸进程的工作
			while(waitpid(-1,NULL,WNOHANG)>0)
				;						
		}
		else{
			cout<<"fork error"<<endl;
			continue;
		}
	}
}

client:

其实就是code4中的client程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <iostream>
#include <signal.h>
using namespace std;

#define MAX 256

int main(void){
	int sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(sockfd<0){
		cout<<"error"<<endl;
		exit(0);
	}

	sockaddr_in srvaddr;
	string ip="127.0.0.1";
	unsigned short port=6000;
	bzero(&srvaddr,sizeof(srvaddr));
	srvaddr.sin_family=AF_INET;
	srvaddr.sin_port=htons(port);
	inet_aton(ip.c_str(),&srvaddr.sin_addr);

	if(connect(sockfd,(sockaddr*)&srvaddr,sizeof(sockaddr))==-1){
		cout<<"connect error"<<endl;
		exit(0);
	}
	cout<<"connected"<<endl;

	char buf[MAX]={0};
	int maxlen=30;
	int len=read(sockfd,buf,30);

	if(len<0){
		cout<<"error"<<endl;
		exit(0);
	}
	buf[len]='\0';
	cout<<buf<<endl;
	close(sockfd);
	return 0;
}

unit 06 进程通信

管道

参考:Linux 的进程间通信:管道 - 知乎 (zhihu.com)

管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间

1. 匿名管道PIPE

特点:

单向通信(半双工)

只适用于父子进程间通信

在进程间实现双向数据传输必须创建两个管道

父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。

函数:

这个方法将会创建出两个文件描述符,可以使用pipefd这个数组来引用这两个描述符进行文件操作。pipefd[0]是读方式打开,作为管道的读描述符。pipefd[1]是写方式打开,作为管道的写描述符从管道写端写入的数据会被内核缓存直到有人从另一端读取为止

int pipe(int pipefd[2]);
output:
    0-成功,-1-失败

使用方法:

fork产生的子进程会继承父进程对应的文件描述符。利用这个特性,父进程先pipe创建管道之后,子进程也会得到同一个管道的读写文件描述符。从而实现了父子两个进程使用一个管道可以完成半双工通信。此时,父进程可以通过fd[1]给子进程发消息,子进程通过fd[0]读。子进程也可以通过fd[1]给父进程发消息,父进程用fd[0]读。

例程:用两个管道实现父子进程间的全双工通信

提一嘴:为什么父进程关了读通道子进程还是能用读通道呢?因为linux OS中的基于索引节点的文件共享方法,即使父进程关闭了读描述符,其对应的文件因为被子进程使用,所以并未被真正关闭。

#include <unistd.h>
#include <iostream>
#include<stdio.h> 
#include<stdlib.h> 
#include<errno.h> 

using namespace std;

const int MAX=30;

int main(void){
	int pipe1[2],pipe2[2];
	char msg1[]="parent to child";
	char msg2[]="child to parent";

	if(pipe(pipe1)<0||pipe(pipe2)<0){
		cout<<"create pipe error"<<endl;
		exit(0);
	}
    
	pid_t pid=fork();
	char buf[MAX];
	if(pid==0){		
		close(pipe1[0]);
		close(pipe2[1]);
		write(pipe1[1],msg1,MAX);
		int len=read(pipe2[0],buf,MAX);
		buf[len]='\0';
		cout<<"child received:"<<buf<<endl;
		exit(0);
	}
	else{
		close(pipe1[1]);
		close(pipe2[0]);
		write(pipe2[1],msg2,MAX);
		int len=read(pipe1[0],buf,MAX);
		buf[len]='\0';
		cout<<"parent received:"<<buf<<endl;
		exit(0);
	}
}

2. 命名管道FIFO

命名管道在底层的实现跟匿名管道完全一致,区别只是命名管道会有一个全局可见的文件名以供其他的进程open打开使用。

函数:

int mkfifo ( char *pathname,mode_t mode );
input:
    pathname-管道名称,路径名
	mode-打开文件的模式。一般设置为O_CREAT|O_EXCL,即没有文件则创建文件,文件已存在就报错到errno中
output:
    0-成功,-1-失败

命名管道使用步骤:

写进程使用mkfifo创建命名管道

写进程调用open以写阻塞方式打开管道

读进程调用open以读阻塞方式打开管道

写进程调用write写入数据

读进程调用read读出数据

关于读写阻塞

管道实际上就是内核控制的一个内存缓冲区,既然是缓冲区,就有容量上限。

在管道中没有数据的情况下,对管道的读操作会阻塞,直到管道内有数据为止。当一次写的数据量不超过管道容量的时候,对管道的写操作一般不会阻塞,直接将要写的数据写入管道缓冲区即可;当要写的数据量大于缓存区空闲容量时,写操作就会被阻塞,直到管道的数据被读取到有足够空间。

例程:

server创建命名管道,然后写阻塞模式打开管道并写入数据

#include<cstdio>
#include<cstring>
#include<string>
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>

using namespace std;

const int MAX=256;

int main(void){
	char buf[]="I am zhg, fuck u xdu";
	string filename="fifo";
	if(mkfifo(filename.c_str(),O_CREAT|O_EXCL)<0 && errno!=EEXIST){
		cout<<"make fifo error"<<endl;
		exit(0);
	}

    //写阻塞模式打开管道
	int fd=open(filename.c_str(),O_WRONLY,0);
	if(fd<0)
		cout<<"open fifo error"<<endl;

	if(write(fd,buf,strlen(buf))<0)
		cout<<"write error"<<endl;
	close(fd);
}

client读阻塞模式打开命名管道,然后从中读数据。注意:启动时需要在root权限下,否则可能权限不够无法打开命名管道文件。

#include<cstdio>
#include<cstring>
#include<string>
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>

using namespace std;

const int MAX=256;

int main(void){
	char buf[MAX]={'\0'};
	string filename="fifo";
	int fd=open(filename.c_str(),O_RDONLY,0);
	if(fd<0)
		cout<<"open fifo error"<<endl;
	int len=read(fd,buf,30);
	if(len<0)
		cout<<"read error"<<endl;
	else{
		buf[len]='\0';
		cout<<"read content:"<<buf<<endl;
	}
	exit(0);
}

Unix域socket

UNIX域协议不是真正的网络协议

UNIX域协议提供同一台机器的进程间通信

UNIX域socket是双向通道

UINIX域socket分为命名和非命名两种,分别和命名管道和非命名管道类似

1. 命名UNIX域socket

地址结构体:

struct socketaddr_un{
	short int sun_family;	//AF_UNIX
	char sun_path[104];	//文件名的绝对路径
};

使用路径名标识服务器和客户端。这里的文件类似于之前的端口

服务器调用函数bind绑定一个UNIX域socket时以该路径名创建一个文件

步骤:

server端:

1.服务器调用socket创建UNIX域socket

2.服务器调用bind绑定UNIX域socket和指定地址

3.服务器调用listen转化为侦听socket

4.服务器调用accept接收客户端连接

client端:

1.客户端创建UNIX域socket(同服务器)

2.客户端调用connect连接服务器

3.客户端和服务器利用UNIX域socket进行通信

例程:

server:

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <errno.h>
#include <stdio.h> 
#include <stdlib.h> 
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h>
#include <string.h>
using namespace std;

const int MAX=256;

int main(void){
	int sockfd=socket(AF_UNIX,SOCK_STREAM,0);
	struct sockaddr_un addr;
	char filename[]="./unix_socket";
	bzero(&addr,sizeof(addr));
	unlink(filename);
    //注:sun_family必须设置为AF_UNIX
	addr.sun_family=AF_UNIX;	
	memcpy(addr.sun_path,filename,sizeof(filename));
	bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
	listen(sockfd,5);
	int new_fd=accept(sockfd,NULL,NULL);

	char buf1[]="server to client";
	char buf2[MAX]={'\0'};
	write(new_fd,buf1,30);
	int len=read(new_fd,buf2,30);
	buf2[len]='\0';
	cout<<buf2<<endl;
}

client:

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <errno.h>
#include <stdio.h> 
#include <stdlib.h> 
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h>
#include <string.h>
using namespace std;

const int MAX=256;

int main(void){
	int sockfd=socket(AF_UNIX,SOCK_STREAM,0);
	struct sockaddr_un addr;
	char filename[]="./unix_socket";
	bzero(&addr,sizeof(addr));
	addr.sun_family=AF_UNIX;
    //和server使用同一个文件
	memcpy(addr.sun_path,filename,sizeof(filename));

	if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0){
		cout<<"connect error"<<endl;
		exit(0);
	}
	cout<<"connected"<<endl;
	char buf1[]="client to server";
	char buf2[MAX]={'\0'};
	int len=read(sockfd,buf2,30);
	if(len<0){
		cout<<"read error"<<endl;
		exit(0);
	}
	buf2[len]='\0';
	cout<<buf2<<endl;
	write(sockfd,buf1,30);
}

2. 非命名UNIX域socket

socket是无名的

socket是全双工的

通信前不需要连接

通常在父子进程间通信使用socketpair

函数:创建两个UNIX域socket,并连接在一起

通常在父子进程间通信使用socketpair

int socketpair(int family,int type,int protocol,int fd[2]);
input:
    family-必须是AF_UNIX
    type-SOCK_STREAM或SOCK_DGRAM
    protocol-0
    fd-存储已创建的socket
output:
    0-成功,-1-失败

例程:

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <errno.h>
#include <stdio.h> 
#include <stdlib.h> 
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <signal.h>
#include <string.h>
using namespace std;

int main(int argc,char **argv)
{
	int sockfd[2];
	char buf[32];
	if(socketpair(AF_UNIX,SOCK_STREAM,0,sockfd)<0)
	{
		cout<<"socketpair error"<<endl;
		return 1;
	}
	pid_t pid=fork();
	if(pid>0)
	{
		close(sockfd[0]);
		send(sockfd[1],"a",1,0);
		recv(sockfd[1],buf,1,0);
		cout<<"a->"<<buf<<endl;
	}
	else if(pid==0)
	{
		close(sockfd[1]);
		recv(sockfd[0],buf,1,0);
		buf[0]=toupper(buf[0]);
		send(sockfd[0],buf,1,0);
		exit(0);
	}
	else
		cout<<"fork error"<<endl;
	return 0;
}

unit 07 I/O模型

阻塞式I/O模型

1. 阻塞的原因

linux进程调度算法-时间片调度算法

2. 产生阻塞的函数

读、写、TCP协议建立连接、TCP协议接受连接

注:UDP协议的写操作、建立连接操作不会阻塞

3. 解决方法

调用alarm函数设置超时

例程:

client

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <unistd.h>
#include <iostream>
#include <stdlib.h> 
#include <string.h>

#define MAX_RECV_SIZE 4096

using namespace std;

int timeout_flag=0;
void sigalrm_handler(int signo);

int main(int argc,char **argv)
{
	int 		sockfd,nbytes;
	char 		recv_buf[MAX_RECV_SIZE];
	struct 	sockaddr_in srvaddr;
	short		port=6000;
	
	bzero(&srvaddr,sizeof(srvaddr));
	srvaddr.sin_family=AF_INET;
	srvaddr.sin_port=htons(port);	
	inet_aton("127.0.0.1",&srvaddr.sin_addr);
	
	if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){
		printf("socket error\n");
		exit(1);
	}
	if(connect(sockfd,(struct sockaddr *)&srvaddr,sizeof(srvaddr))==-1)	{
		printf("connect error\n");
		exit(1);
	}
	struct sigaction act;
	act.sa_handler=sigalrm_handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	sigaction(SIGALRM,&act,NULL);
	
	for(;;){
		timeout_flag=0;
		alarm(5);//设定5秒超时
		nbytes=read(sockfd,recv_buf,MAX_RECV_SIZE);
		alarm(0);//注意:要取消超时
		if(nbytes<0&&errno==EINTR){
			if(timeout_flag==1)
				cout<<"read timeout"<<endl;
			else
				continue;//被其他信号中断
		}
		else{
			recv_buf[nbytes]=0;
			cout<<"recv:"<<recv_buf<<endl;
			break;
		}
	}
	close(sockfd);
	return 0;
}

//自定义终端处理函数,防止SIGALRM信号把进程终结
void sigalrm_handler(int signo)
{
	timeout_flag=1;
}

server:延时六秒后发送数据

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXDATASIZE 128
#define BACKLOG 5

int main(int argc,char **argv){
	int sockfd,new_fd,sin_size;
	int port=6000,delay=6;
	char buf[MAXDATASIZE];
	struct sockaddr_in srvaddr,clientaddr;
	
	//1.创建网络端点
	sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(sockfd==-1){
		printf("can't create socket\n");
		exit(1);
	}
	//填充地址
	bzero(&srvaddr,sizeof(srvaddr));
	srvaddr.sin_family=AF_INET;
	srvaddr.sin_port=htons(port);
	srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);
	//2.绑定服务器地址和端口
	if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
		printf("bind error\n");
		exit(1);
	}
	//3. 监听端口
	if(listen(sockfd,BACKLOG)==-1){
		printf("listen error\n");
		exit(1);
	}
	for(;;){
		//4.接受客户端连接
		sin_size=sizeof(struct sockaddr_in);
		if((new_fd=accept(sockfd,(struct sockaddr *)&clientaddr,(socklen_t*)&sin_size))==-1){
			printf("accept error\n");
			continue;
		}
		printf("client addr:%s %d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
		//延时
		sleep(delay);
		//6.回送响应
		sprintf(buf,"welcome!");
		write(new_fd,buf,strlen(buf));
		//关闭socket
		close(new_fd);
	}
	close(sockfd);	
	return 0;
}

运行结果:

root@zhg-virtual-machine:/home/zhg/文档/net_program/unit07_alarmio# ./client
read timeout
recv:welcome!

非阻塞式I/O模型

可以设置socket为非阻塞模式,在非阻塞模式socket上进行I/O操作时,如果操作不能完成,将以错误返回。

函数:

//将socket设置为非阻塞模式
int flag=fcntl(sockfd,F_SETFL,0);
fcntl(sockfd,F_SETFL,flag|O_NONBLOCK);

4种I/O操作返回的错误类型:

读操作:接收缓冲区无数据时返回EWOULDBLOCK

写操作:发送缓冲区无空间时返回EWOULDBLOCK;空间不够时部分拷贝,返回实际拷贝字节数

建立连接:启动3次握手,立刻返回错误EINPROGRESS;服务器客户端在同一主机上connect立即返回成功

接受连接:没有新连接返回EWOULDBLOCK

例程:

while(true) {
	if(read(sockfd,buf,nbytes)<0){
		if(errno==EWOULDBLOCK)	continue;
		else{
			printf(“read error\n”);
			break;
		}
	}
	else 
		do something...;
}

输入输出多路复用I/O模型

核心:使用select函数判断多个套接字的状态。

信号驱动I/O

适用于UDP协议

主要步骤

1.设置SIGIO信号处理函数

2.设置socket描述符所有者

3.允许这个socket进行信号驱动I/O

模板:

void sigio_handler(int signo){
	…
}

int main(){
    int sockfd;
    int on=1;
    …
    signal(SIGIO,sigio_handler);	//sigaction也可以
    fcntl(sockfd,F_SETOWN,getpid());	//设置套接字描述符所有者为当前进程
    ioctl(sockfd,FIOASYNC,&on);		//允许这个socket进行信号驱动
    …
}

unit 08 服务器模型

循环服务器

同一时刻只能处理一个客户端请求

UDP服务器通常采用循环服务器模型

并发服务器

同一时刻可以处理多个客户端请求

TCP服务器通常采用并发服务器模型

模型1:一个子进程对应一个客户端

注:父进程fork后要关闭socket描述符,子进程结束后必须终结进程

模型2:延迟创建子进程

循环与并发的结合,平时服务器工作在循环状态,当预测某一次服务耗时较长就为它创建一个子进程,而主进程继续工作在循环模式中。可设置预测器。

模型3:预创建子进程

posted @ 2022-07-01 20:51  带带绝缘体  阅读(71)  评论(0)    收藏  举报