网络编程三---多线程/进程解决并发问题

前文列出的代码给大家展示了一个最简单的网络程序,但正如文章末尾所提的,这个最简单的网络程序最大的缺点是服务端一次只能服务一个客户端,就比如说你去吃饭,饭店只有一个服务员, 而且服务员在客户离开之前只能为一个客户服务,也就是说你只能等待你前面的客户吃好饭离开了,然后你才能进去吃饭,而在你吃饭的时候时候,你后面来的人都得等你吃完饭才能轮到你后面一个人吃饭。这种模式的缺点很明显,因为在你进去点好菜到买单前的这段时间,这个服务员都是空闲的,为什么不让服务员在这个空闲时间让其他客户进来服务员为他点菜呢?在网络编程中,这个思想是类似的,前面的代码服务端与客户端建立连接后,服务端都在为这个客户端服务,而有可能这个客户端在当时正在等待用户输入,而服务端在等待客户端数据的到达,有什么办法可以让服务端在这个等待的时间服务其他客户端呢?答案是肯定的。解决这个问题的思路有两个:

1、服务端分配多个线程/进程来处理客户端连接,一个客户端对应一个线程/进程

2、服务端的一个进程/线程对应多个连接.

仍然用到饭店吃饭来理解这个思路,思路一是餐厅招聘多个服务员,一个服务员服务一个客户,每来一个客户分配一个服务员专门为他服务。思路二是一个服务员服务多个客户,服务员可以给一个客户点好菜后给另一个客户买单,再给另外一个客户上菜。显而易见,思路一给餐厅增加了成本,来一个客户就需要一个服务员为他服务,当服务员人数既定的情况下就只能服务有限的客户,而思路二在既定服务员人数的情况下却可以进来多的服务客户。

下面分别本篇文章和下篇文章来分别介绍思路一和思路二

多进程/线程解决并发(思路一)

前文的服务端程序一次只能为一个客户端服务,那么按照思路一的方法,当一个客户端请求来时服务端分配一个线程或进程专门为这个客户端服务,带服务结束后线程或进程结束。

多线程解决并发

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

void* doRead(void* arg)
{
	long connfd = (long)arg;
	char recvBuf[101] = "";
	int n = 0;
    while((n = recv(connfd,recvBuf, sizeof(recvBuf),0 )) > 0)
    {
        printf("number of receive bytes = %d.\n", n);
		//发送数据
        send(connfd, recvBuf, n, 0);
		char* bufTmp = recvBuf;
        while(*bufTmp != ‘\r‘ && *bufTmp !=‘\n‘)
			bufTmp++;
		*bufTmp = ‘\0‘;
        if(strcmp(recvBuf, "quit") == 0)
        {
            break;
        }
    }
	close(connfd);

	return NULL;
}
int main()
{
	struct sockaddr_in sockaddr;
	pthread_t thread_id;
	int one =1;
	int ret = 0;
	bzero(&sockaddr, sizeof(sockaddr));
	sockaddr.sin_family = AF_INET;
	sockaddr.sin_port = htons(8080);
	sockaddr.sin_addr.s_addr  = htonl(INADDR_ANY);
	long clientfd;
	int listenfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个socket
	//将该套接字的绑定端口设为可重用
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void*) &one,(socklen_t)sizeof(one));
	if(bind(listenfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) < 0)
	{
		perror("bind error");
		return -1;
	}

	ret = listen(listenfd, 16);
	if(ret < 0)
	{
		perror("listen failed.\n");
		return -1;
	}

    struct sockaddr_in clientAdd;
    socklen_t len = sizeof(clientAdd);
	while(1)
	{
		clientfd = accept(listenfd, (struct sockaddr *)&clientAdd, &len);
		if(clientfd < 0)
		{
			perror("accept this time");
			continue;
		}
		if(clientfd > 0)
		{
			//由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,值传递
			pthread_create(&thread_id, NULL, doRead, (void *)clientfd);
			printf("thread %d created.\n",thread_id );
			pthread_detach(thread_id);
		}
	}
	close(listenfd);
	return 0;
}

程序每当客户端发起一个连接时,服务端接受客户端连接accpet返回成功后,都创建一个线程,并将返回的客户端的连接描述符fd传递给线程处理函数,在线程的处理函数doRead处理客户端数据的收发。需要注意的是pthread_create传递的第四个参数传递的是将long类型的数据强转成void *类型的数据,为什么是long类型不是int型呢?这个在没个机器上可能都不一样,在笔者的机器上,int是4个字节,而void*类型是8个字节,所以必须将socket描述符定义成8个字节long类型,否则int型装成long类型编译器报错,此外需要注意的是这里传递的不是clientfd的地址,如果传递了clientfd的地址,那么每当来一个连接的时候,变了clientfd都会被改写,而线程是共享进程的内存空间的,也就是说吐过传递了clienfd的地址所有线程都会使用同一个clienfd,到时其他客户端无法和服务端通信。

多线程解决并发的优缺点:

优点:使用多线程解决并发的优点是编码比较简单,代码量少,同时响应比较快。

缺点:使用多线程的缺点比较多,多线程解决并发受限于系统资源,创建一个线程默认要创建1M大小的栈空间,而一个进程的地址空间是2G,因此一个进程最多创建2000个线程,此外线程创建,销毁,切换都需要消耗系统资源,因此线程数量到一定程度后哪怕增加系统资源也无法增加线程数量,对于大的并发量无法支持。如果程序间需要涉及到数据的同步互斥,那么整个逻辑会比较麻烦,线程间的资源同步互斥比较难控制,除了问题也比较难调试。一个线程崩溃了可能引起整个进程的崩溃,进而影响服务器性能。

多进程解决并发

思路一的另一方法师用多进程来解决并发,每来一个客户端请求,创建一个子线程,子线程服务客户端请求,父线程继续监听客户端连接。

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

void doread(int fd)
{
	int n = 0;
	char buf[101] = {0};
	while((n = recv(fd, buf, sizeof(buf), 0))> 0)
	{
		printf("msg recv is %s\n", buf);
		send(fd,buf, sizeof(buf), 0);
		char* bufTmp = buf;
        while(*bufTmp != ‘\r‘ && *bufTmp !=‘\n‘)
			bufTmp++;
		*bufTmp = ‘\0‘;
        if(strcmp(buf, "quit") == 0)
        {
            break;
        }
	}
}

int main()
{
	struct sockaddr_in SerAddr;
	SerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	SerAddr.sin_port = htons(12345);
	SerAddr.sin_family = AF_INET;

	int listenfd = socket(AF_INET,SOCK_STREAM,0);
	if(bind(listenfd, (struct sockaddr*)&SerAddr, sizeof(SerAddr)) < 0)
	{
		perror("bind failed");
		return -1;
	}

	if(listen(listenfd, 64) < 0)
	{
		perror("listen failed\n");
		return -1;
	}

	while(1)
	{
		int connfd;
		struct sockaddr_in clientAddr;
		socklen_t len = sizeof(clientAddr);
		connfd = accept(listenfd, (struct sockaddr*)&clientAddr, &len);
		if(connfd < 0)
			continue;
		if(fork() == 0)
		{
			close(listenfd);
			doread(connfd);
			close(connfd);
		}

		close(connfd);

	}
	close(listenfd);

}

从代码中可以看到,父进程创建一个监听socket,并监听来自客户端的请求,每当来一个客户端请求时,创建一个子进程,子进程doread方法处理与客户端的通信。需要注意的是,在子进程创建的开始的时候需要调用close(listenfd),结束时调用close(connfd),而在父进程中调用close(connfd),这是由于父进程和子进程共享资源,而每个文件描述符都有一个引用计数,当fork成功时,listenfd和connfd的引用计数都会变为2,而close函数将判断当前描述符的引用计数,若引用计数为1时才关闭描述符,否则仅将引用计数减1.因此子进程中需要先将listenfd的引用计数减1,,父进程中将connfd的引用计数减1.

多进程解决并发的优缺点

多进程的优点:每个进程相互独立,一个进程的崩溃不会引起另外进程崩溃。通过增加CPU就可以提高性能,且没有同步互斥的复杂控制逻辑。

多进程缺点:进程的创建销毁消耗系统资源,此外如果有跨进程的数据通信就比较复杂,使用只有小数据量的进程间通信的场景,进程的多少受限于系统资源。

总结

虽然多线程/进程可以在一定程度上解决并发,但由于受限于系统资源,解决并发的能力有限,多线程、进程的创建销毁和切换都需要消耗系统资源,此外类似的线程同步互斥,进程间通信就逻辑比较复杂,出问题很难调试。因此在网络大规模应用的今天,通过多线程/进程来解决并发并不合适。因此有人用线程池计数来解决上述的限制,如传统的Apache服务器貌似就是用这种线程池的池化技术,也有将多线程和多进程结合起来的方法,虽然一定程度上提高了并发,但都有一定的局限性。

说明:本系列的例子中有资源的代码

        while(*bufTmp != ‘\r‘ && *bufTmp !=‘\n‘)
			bufTmp++;
		*bufTmp = ‘\0‘;

那是因为笔者的编程环境是虚拟机+ssh,通过SSH发送的数据发现字符串末尾都带了"\r\n"回车换行字符,因此需要与字符串quit进行比较的时候需要作转换,这部分代码仅用于去掉字符串末尾的‘\r‘ ‘\n\’字符。

时间: 2024-12-14 18:04:42

网络编程三---多线程/进程解决并发问题的相关文章

网络编程模型及网络编程三要素

网络模型 计算机网络之间以何种规则进行通信,就是网络模型研究问题. 网络模型一般是指 OSI(Open SystemInterconnection开放系统互连)参考模型 TCP/IP参考模型 网络模型7层概述: 1.物理层:主要定义物理设备标准,如网线的接口类型.光纤的接口类型.各种传输介质的传输速率等.它的主要作用是传输比特流(就是由1.0转化为电流强弱来进行传输,到达目的地后在转化为1.0,也就是我们常说的数模转换与模数转换).这一层的数据叫做比特. 2. 数据链路层:主要将从物理层接收的数

iOS网络编程(三) 异步加载及缓存图片----&gt;SDWebImage

@SDWebImage提供一个UIImageView的类别以支持加载来自网络的远程图片.具有缓存管理.异步下载.同一个URL下载次数控制和优化等特征. @SDWebImage的导入1.https://github.com/rs/SDWebImage 下载SDWebImage开源包2.将类包拖入工程,再导入MapKit.framework.ImageIO.framework两个框架3.SDWebImage是支持ARC的,在MRC的工程中要注意,可参考MRC工程配置ARC4.注意:SDWebImag

网络编程三素概述

1.1网络编程概述计算机网络 ●是指将地理位置不同的具有独立功能的多 台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统 网络编程●在网络通信协议下, 实现网络互连的不同计算机上运行的程序间可以进行数据交换 网络编程三要素 IP地址●要想让网络中的计算 机能够互相通信,必须为每台计算机指定一个标识号, 通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而IP地址就是这个标识号.也就是设备的标识 端口●网

[Python网络编程]浅析守护进程后台任务的设计与实现

在做基于B/S应用中,经常有需要后台运行任务的需求,最简单比如发送邮件.在一些如防火墙,WAF等项目中,前台只是为了展示内容与各种参数配置,后台守护进程才是重头戏.所以在防火墙配置页面中可能会经常看到调用cgi,但真正做事的一般并不是cgi,比如说执行关机命令,他们的逻辑如下: (ps:上图所说的前台界面包含通常web开发中的后端,不然也没有socket一说) 为什么要这么设计 你可能疑惑为什么要这么设计,我觉得理由如下: 首先有一点说明,像防火墙等基本上都运行在类Linux平台上 1.安全问题

winform网络编程(三)

TcpClient类和TcpListener类 (1)TcpClient的用途: 用于在同步阻止模式下通过网络来链接.发送和接受流数据,在此情况下,必须有侦听此连接的请求,而侦听的任务就交给TcpListener实例或Socket实例 (2)TcpClient的两种方法连接到侦听器 第一种:创建一个TcpClient,并调用3个可用的Connect方法之一 第二种:使用远程主机的主机名和端口号创建TcpClient,此构造函数将自动尝试一个连接 (3)TcpClient的常用属性和方法 Avai

C#网络编程基础之进程和线程详解

在C#的网络编程中,进程和线程是必备的基础知识,同时也是一个重点,所以我们要好好的掌握一下. 一:概念 首先我们要知道什么是"进程",什么是"线程",好,查一下baike. 进程:是一个具有一定独立功能的程序关于某个数据集合的一次活动.它是操作系统动态执行的基本单元, 在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元. 线程:是"进程"中某个单一顺序的控制流. 关于这两个概念,大家稍微有个印象就行了,防止以后被面试官问到. 二:进程

Python网络编程03/ low版解决粘包问题

目录 Python网络编程03/ low版解决粘包问题 1.操作系统的缓存区 2.基于TCP协议的socket循环通信 2.1 服务端(server) 2.2客户端(client) 3.基于TCP协议的socket链接+循环 通信 3.1服务端(server) 3.2 客户端(client) 4.基于TCP协议的socket应用实例:执行远程命令 4.1服务端(server) 4.2客户端(client) 5.粘包现象 5.1服务端(server) 5.2客户端(client) 5.3展示收发问

Java高并发网络编程(三)NIO

从Java 1.4开始,Java提供了新的非阻塞IO操作API,用意是替代Java IO和Java Networking相关的API. NIO中有三个核心组件: Buffer缓冲区 Channel通道 Selector选择器 一.Buffer缓冲区 缓冲区本质上是一个可以写入数据的内存块(类似数组),然后可以再次读取.此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用内存块. 相比较直接对数组的操作,BufferAPI更容易操作和管理. 使用Buffer进行数据写入

第九篇:网络编程补充与进程

本篇内容 udp协议套接字 开启进程的方式 多进程实现并发的套接字通信 join方法 守护进程 同步锁 进程队列 生产者消费者模型 进程池 paramiko模块 一. udp协议套接字 1.TCP和UDP在传输层区别:UDP是无连接不可靠的数据报协议.TCP提供面向连接的可靠字节流. 2.使用UDP常见应用:DNS(域名系统),NFS(网络文件系统),SNMP(简单网络管理协议). 3.代码应用: 服务端: #!/usr/binl/env python #encoding: utf-8 #aut