多进程并发服务器代码详解

根据这篇文章pid_t的定义
知道了
pid_t的真实定义:实际他就是 int 类型的。

下面代码是在上一篇错误函数封装服务器客户端通信
基础上,升级后的简易的多并发代码,就是说,前面只能开一个客户端和服务器通信,下面的代码可以同时开多个客户端和服务器通信。

注意看代码里面fork的用法。这个单词就是克隆的意思,可以百度查一查具体怎么用。
里面还多了一个清零函数。

最后注意在wrap.h中把头文件添加上就好。

#include"wrap.h"

#define SERV_PORT 9527 //bind函数中的端口号

char buf[BUFSIZ], clint_IP[1024];//用来保存读到的客户端的数组,BUFSIZ默认4096
//上面第二个数组用来接收客户端IP地址

//void sys_err(const char *str)
//{
//	//perror是用来输出错误的,如果某些函数调用不正确的话,调用perror会先输出错误号,然后输出你在perror()参数中指定的内容
//	perror(str);//包含在stdlib.h这个头文件中
//	exit(1);
//}

int main(int argc, char *argv[])
{
	int ifd =0,cfd=0,ret;//保存文件描述符,用于后期和服务器建立连接
	pid_t pid;//注意这个的定义类型
	socklen_t clit_addr_len;

	
	//定义bind函数中用到的ip地址结构体
	struct sockaddr_in serv_addr, clit_addr;

	bzero(&serv_addr,sizeof(serv_addr));//把从第一个参数地址起,长度为第二个参数的字节清零

	serv_addr.sin_family = AF_INET;//协议簇地址,和第三个参数的区别在于,这个是地址类型,第三个是有效实际地址
	//TCP/IP是一个网络通讯协bai议群,它包含了很多通信协议。这些协du议制定了网zhi络设备、计算机连入网络以及数据是如何dao在它们之间进行传输的标准。
	//TCP协议又叫传输控制协议,作用于传输层。IP协议又叫网络间互联协议,工作在网络层。它们都是TCP/IP协议簇中非常重要的协议。
	serv_addr.sin_port = htons(SERV_PORT);//端口这里需要字节序的转换!需要来回传的都需要转换!
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//取出系统中任意有效的IP地址

	//sin_family和sin_addr.s_addr的地址有什么区别?

	//socket目的:创建一个套接字
	//第一个参数是个地址,IPv4的地址。用来产生套接字ip地址的协议
	//第二个参数是个协议,数据传输协议
	//第三个参数是:你所选用的数据传输协议的代表协议,比如流式数据传输协议(SOCK_STREAM)的代表协议是tcp
	//成功:返回指向新创建的socket的文件描述符,失败:返回-1,
	ifd = Socket(AF_INET, SOCK_STREAM, 0);
	

	//bind作用:将socket和bind的第二个参数中的地址、端口号捆绑起来,以便监听
	//第一个参数套接字描述符、  第二个参数是个结构体指针,里面包含服务器的IP地址+端口号、第三个参数是这个地址的长度
	Bind(ifd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));//和上面返回描述符不同,这个返回的是0

	Listen(ifd, 128);//第二个参数是它所能监听的最大上限数

	clit_addr_len=sizeof(clit_addr);

	while(1)
	{
		//第二个参数是接收链接客户端地址信息,含IP地址和端口号,所以前面不需要专门定义
		//返回一个套接字
		cfd=Accept(ifd, (struct sockaddr*)&clit_addr ,&clit_addr_len);//注意第三个参数类型,相比第bind多一个指针
		 pid= fork();

		if(pid<0)
		{
			perr_exit("fork error");
			//exit(1);
		}else if(pid==0)//子进程
		{
				
					Close(ifd);//子进程关闭监听
					break;
				
		}else //父进程
		{
			close(cfd);//父进程关闭通信
			continue;
		}
	}
		if(pid==0)
		    {
				for(;;)
				{
					ret=Read(cfd,buf,sizeof(buf));//返回字节数
					if(ret==0)
					{ 
					close(cfd);
					exit(1);
					}
					Write(STDOUT_FILENO,buf,ret);标准输出到屏幕
					for(int i=0; i<ret;i++)
					buf[i]=toupper(buf[i]);
					
					Write(cfd,buf,ret);//把buf的内容写回到客户端
				}
	      }
	
	return 0;
}

wrap.h的头文件改动:

#ifndef __WRAP_H_
#define __WRAP_H_ //防止重复定义

#include<stdio.h>//输入输出
#include<stdlib.h>//动态内存分配
#include<string.h>
#include<strings.h>//针对bzero这个清零头文件
//linux下有这个头文件,vc下没有
//它包含很多UNIX系统服务的函数原型,eg:read write函数等
#include<unistd.h>
#include<signal.h>//有关信号的函数
#include<errno.h>
#include<pthread.h>//包含有关多线程的函数

#include <sys/socket.h>//套接字函数、bind函数头文件
#include<ctype.h>//转小写转大写函数的头文件,在第二个地址结构的时候用
#include<arpa/inet.h>//bind的头文件

void perr_exit(const char *s);
int Socket(int family, int type, int protocol);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);

//int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);

ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);


#endif

上面的函数注意:每一次退出客户端都是crtl+c,这样退出之后会产生僵尸进程,所以需要想办法改进

僵尸进程就是父进程没死,子进程死掉,子进程残留出一点程序,等着父进程给自己收尸,如果不管的话,残留的越来越多,就会对系统造成影响。

下面涉及到的函数,都在这里的链接处找到系统的学习一下,再回来看:
黑马Linux系统编程视频

一、

*wait(int status)函数

参数为传出参数,返回的是子进程得状态。

该函数得作用:
通俗讲解:
1.等待它的儿子(子进程)运行完。
2.帮他儿子收尸。
3.知道它儿子死的原因。

一般放在父进程里面!

针对第三个功能:

一般借助宏函数判断怎么死的:

1.WIFEXITED(status)为非0-进程正常结束(大写得含义wait if exited)

ExITSTATUS(status)如上宏为真,使用此宏一获取进程退出状态(有状态11,17等)(exit status)

2.WIFSIGNALED(status)为非0一进程异常终止(wait if signaled)

WTERMSIG(status)如上宏为真,使用此宏一取得使进程终止的那个信号的编号。(wait term signaled)

*因为一次wait(int status)函数只能回收一个子进程,所以
延伸一个函数:
*pid_t waitpid(pid_t pid,int status,int options);

这个函数的目的是:暂时停止目前进程的执行,直到有信号来到或子进程结束。子进程的结束状态值会由参数 status 返回。如果不在意结束状态值,则参数 status 可以设成 NULL。而子进程的进程识别码也会一起返回。参数 pid 为欲等待的子进程识别码。

头文件:
#include<sys/types.h>
#include<sys/wait.h>

返回:
成功:返回清理掉的子进程ID;
失败:-1(无子进程)

三个参数的定义:

  参1:    pid  > 0       指定进程id(子进程识别码)回收

                 pid = -1        回收任意子进程

                 pid = 0         回收本组任意子进程

                 pid < -1        回收该进程组的任意子进程

   参2:    status:

                 代表返回:      成功:pid       失败 -1

                 status:传出参数

                 1: 阻塞等待子进程

                 2: 回收子进程资源

                 3:  获取子进程结束状态:1)WIFEXITED()真

                 WEXITSTATUS()获取子进程退出状态

               2)WIFSIGNALED() 真

              WTERMSIG()获取导致子进程终止的信号的编码

参数3

options提供了一些额外的选项来控制waitpid,参数 option 可以为 0 或可以用"|"运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

如果我们不想使用它们,也可以把options设为0,如:
ret=waitpid(-1,NULL,0);

WNOHANG情况:
若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
WUNTRACED 情况
若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。WIFSTOPPED(status)宏确定返回值是否对应与一个暂停子进程。

二、

第二个sigaction函数

**int sigaction(int signum, const struct sigaction act,
struct sigaction oldact);

这个函数的目的:检查或修改与指定信号相关联的处理动作(可同时两种操作)

头文件:
#include <signal.h> #include<sys/types.h>
#include<sys/wait.h>
这几个头文件即可。

参数的意义:

signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。

回收子进程僵尸进程的代码:
其实就是加了上面两个函数,只不过这两个函数不太好理解,需要看视频好好学习一下就可以。同时记得在头文件里面加上#include <signal.h>

#include"wrap.h"

#define SERV_PORT 9527 //bind函数中的端口号

char buf[BUFSIZ], clint_IP[1024];//用来保存读到的客户端的数组,BUFSIZ默认4096
//上面第二个数组用来接收客户端IP地址

//void sys_err(const char *str)
//{
//	//perror是用来输出错误的,如果某些函数调用不正确的话,调用perror会先输出错误号,然后输出你在perror()参数中指定的内容
//	perror(str);//包含在stdlib.h这个头文件中
//	exit(1);
//}

void docatch(int signo)
{
	//第一个参数是回收任意一个子进程
	//第二个参数是进程结束状态返回,不用管
	//第三个代表轮询返回,如果进程还没结束,返回0,结束了返回对应ID
	while(waitpid(0,NULL,WNOHANG));
	return;
}
int main(int argc, char *argv[])
{
	int ifd =0,cfd=0,ret;//保存文件描述符,用于后期和服务器建立连接
	pid_t pid;//注意这个的定义类型
	socklen_t clit_addr_len;

	
	//定义bind函数中用到的ip地址结构体
	struct sockaddr_in serv_addr, clit_addr;

	bzero(&serv_addr,sizeof(serv_addr));//把从第一个参数地址起,长度为第二个参数的字节清零

	serv_addr.sin_family = AF_INET;//协议簇地址,和第三个参数的区别在于,这个是地址类型,第三个是有效实际地址
	//TCP/IP是一个网络通讯协bai议群,它包含了很多通信协议。这些协du议制定了网zhi络设备、计算机连入网络以及数据是如何dao在它们之间进行传输的标准。
	//TCP协议又叫传输控制协议,作用于传输层。IP协议又叫网络间互联协议,工作在网络层。它们都是TCP/IP协议簇中非常重要的协议。
	serv_addr.sin_port = htons(SERV_PORT);//端口这里需要字节序的转换!需要来回传的都需要转换!
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//取出系统中任意有效的IP地址

	//sin_family和sin_addr.s_addr的地址有什么区别?

	//socket目的:创建一个套接字
	//第一个参数是个地址,IPv4的地址。用来产生套接字ip地址的协议
	//第二个参数是个协议,数据传输协议
	//第三个参数是:你所选用的数据传输协议的代表协议,比如流式数据传输协议(SOCK_STREAM)的代表协议是tcp
	//成功:返回指向新创建的socket的文件描述符,失败:返回-1,
	ifd = Socket(AF_INET, SOCK_STREAM, 0);
	

	//bind作用:将socket和bind的第二个参数中的地址、端口号捆绑起来,以便监听
	//第一个参数套接字描述符、  第二个参数是个结构体指针,里面包含服务器的IP地址+端口号、第三个参数是这个地址的长度
	Bind(ifd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));//和上面返回描述符不同,这个返回的是0

	Listen(ifd, 128);//第二个参数是它所能监听的最大上限数

	clit_addr_len=sizeof(clit_addr);

	while(1)
	{
		//第二个参数是接收链接客户端地址信息,含IP地址和端口号,所以前面不需要专门定义
		//返回一个套接字
		cfd=Accept(ifd, (struct sockaddr*)&clit_addr ,&clit_addr_len);//注意第三个参数类型,相比第bind多一个指针
		 pid= fork();

		if(pid<0)
		{
			perr_exit("fork error");
			//exit(1);
		}else if(pid==0)//子进程
		{
				
					Close(ifd);//子进程关闭监听
					break;
				
		}else //父进程
		{
			struct sigaction act;
			act.sa_handler = docatch;
			sigemptyset(&act.sa_mask);//清空这个信号集,全变为0
			//sigaddset();
			act.sa_flags =0;//默认为0,信号中断函数执行期间默认屏蔽该信号

			//第一个参数是信号类型,这个看我发的视频链接的信号部分,信号就相当于中断
			//SIGCHID信号就被放到这里面,一旦产生这个信号,就运行你自己指定的函数程序
			//每次状态改变,子进程会发SIGCHID给父进程
        //子进程终止(即子进程死亡)
        //· 子进程停止(即子进程暂停)
        //· 子进程恢复(即子进程从暂停中恢复执行)
		int ret=sigaction(SIGCHLD,&act,NULL);
		if(ret!=0){ perr_exit("sigaction error");}
			close(cfd);//父进程关闭通信
		}
	}
		if(pid==0)
		    {
				for(;;)
				{
					ret=Read(cfd,buf,sizeof(buf));//返回字节数
					if(ret==0)
					{ 
					close(cfd);
					exit(1);
					}
					Write(STDOUT_FILENO,buf,ret);标准输出到屏幕
					for(int i=0; i<ret;i++)
					buf[i]=toupper(buf[i]);
					
					Write(cfd,buf,ret);//把buf的内容写回到客户端
				}
	      }
	
	return 0;
}

下面函数先不用看。

三、

线程创建函数:
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,
void *(start_rtn)(void),void *arg);

头文件:#include <pthread.h>

仔细看,是四个参数!

参数
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。

返回值
若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。

四、

int pthread_join(pthread_t thread, void **retval);

视频讲:专用于回收子线程。
函数pthread_join目的:用来等待一个线程的结束,线程间同步的操作

头文件 : #include <pthread.h> (和创建函数一样!)

参数 1:thread: 线程标识符,即线程ID,标识唯一线程。
传出参数,接收传出来的线程ID
参数2:retval: 用户定义的指针,用来存储被等待线程的返回值。
传出参数,回收线程结束的状态!

返回值 :
0代表成功。
失败则返回的则是错误号。

五、

int pthread_detach(pthread_t thread);

函数目的:从状态上实现线程分离,注意不是指该线程独自占用地址空间。
该函数分析

暂时还不知道具体怎么用,就知道跟产生僵尸进程之类的有关。

参数 :thread: 线程标识符 (和上面的第一个参数一样)

返回:
成功:0;
失败:错误号

六、

void *memset(void *s, int ch, size_t n);

函数目的:将s中当前位置后面的n个字节用 ch 替换并返回 s 。它是对较大的结构体或数组进行清零操作的一种最快方法。

头文件; #include<string.h>

原型:extern void bzero(void *s, int n);

用法:#include <string.h>

功能:置字节字符串s的前n个字节为零。

说明:bzero无返回值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值