网络编程socket(下)(三)

简介: 网络编程socket(下)(三)

四、线程池版TCP网络程序

单纯多线程存在的问题

  • 每当有新连接到来时,服务端的主线程都会重新为该客户端创建为其提供服务的新线程,而当服务结束后又会将该新线程销毁。这样不仅麻烦,且效率低下,每当连接到来的时候服务端才创建对应提供服务的线程
  • 若有大量的客户端连接请求,此时服务端要为每一个客户端创建对应的服务线程。计算机中的线程越多,CPU的压力就越大,因为CPU要不断在这些线程之间来回切换,此时CPU在调度线程的时候,线程和线程之间切换的成本就会变得很高。此外,一旦线程太多,每一个线程再次被调度的周期就变长了,而线程是为客户端提供服务的,线程被调度的周期变长,客户端也体验也会变差

解决方案

  • 可以在服务端预先创建一批线程,当有客户端请求连接时就让这些线程为客户端提供服务,此时客户端一来就有线程为其提供服务,而不是当客户端来了才创建对应的服务线程
  • 当某个线程为客户端提供完服务后,不要让该线程退出,而是让该线程继续为下一个客户端提供服务,若当前没有客户端连接请求,则可以让该线程先进入休眠状态,当有客户端连接到来时再将该线程唤醒
  • 服务端创建的这一批线程的数量不能太多,CPU的压力也就不会太大。此外,若有客户端连接到来,但此时这一批线程都在给其他客户端提供服务,这时服务端不应该再创建线程,而应该让这个新来的连接请求在全连接队列进行排队,等服务端这一批线程中有空闲线程后,再将该连接请求获取上来并为其提供服务

引入线程池

要解决问题就需在服务端引入线程池,线程池的存在就是为了避免处理短时间任务时创建与销毁线程的代价,还能够保证内核充分利用,防止过分调度(调度周期过长)

在线程池中存在一个任务队列,当有新的任务到来的时候,就可以将任务Push到线程池中,在线程池中默认创建10个线程,这些线程不断检测任务队列中是否有任务,若有任务就取出任务,然后调用该任务对应的Run函数对该任务进行处理,若线程池中没有任务当前线程就会进入休眠状态

下面直接将线程池的代码接入到当前的TCP服务器,下面只会讲解线程池接入的方法,若对线程池的实现有疑问的可以去阅读博主的博客《理解与实现线程池》

服务类新增线程池成员

服务端引入线程池,因此在服务类中需要新增一个指向线程池的指针成员:

  • 在构造线程池对象时可以指定线程池中线程的个数,此时默认线程的个数为10
  • 构造线程池时,线程池中的若干线程就会创建出来,而这些线程创建出来后就会不断检测任务队列,从任务队列中取出任务进行处理
  • 当服务进程调用accept函数获取到一个连接请求后,就会根据该客户端的套接字、IP地址以及端口号构建出一个任务,然后调用线程池提供的Push接口将该任务塞入任务队列

实际上就是一个生产者消费者模型,其中服务进程就作为了任务的生产者,而后端线程池中的若干线程就不断从任务队列当中获取任务进行处理,承担的就是消费者的角色,其中生产者和消费者的交易场所就是线程池中的任务队列

class TcpServer
{
public:
    TcpServer(uint16_t port):_socket_listen_fd(-1),_server_port(port),_thread_pool(ThreadPool<Task>::GetThreadPool()) {}
    void InitServer();
    void StartUp();
    static void* HandlerClient(void*);
    static void Service(int, string, uint16_t);
    ~TcpServer();
private:
    int _socket_listen_fd;
    uint16_t _server_port;
    unique_ptr<ThreadPool<Task>> _thread_pool;
};
void TcpServer::StartUp()
{
    _thread_pool->Run();//启动线程池
    while(true) 
    {
        //获取连接
        struct sockaddr_in foreign;
        memset(&foreign, '\0', sizeof foreign);
        socklen_t length = sizeof foreign;
        int server_socket_fd = accept(_socket_listen_fd, (struct sockaddr*)&foreign, &length);
        if(server_socket_fd < 0) {
            cerr << "accept fail" << endl;
            continue;
        }
        string client_ip = inet_ntoa(foreign.sin_addr);
        uint16_t client_port = ntohs(foreign.sin_port);
        cout << "New Link: [" << server_socket_fd << "] [" <<  client_ip << "] [" << client_port << "]" << endl;
        //构造任务并推送到任务队列中
        Task task(server_socket_fd, client_ip, client_port, Service);
        _thread_pool->PushTask(task);
    }
}

设计任务类

该任务类中需要包含accept客户端对应的套接字、IP地址、端口号,表示该任务是为哪一个客户端提供服务,对应操作的套接字是哪一个

任务类中需包含一个仿函数方法,当线程池中的线程取到任务后就会直接调用仿函数对该任务进行处理,而实际处理这个任务的方法是服务类中的Service函数,服务端就是通过调用Service函数为客户端提供服务的

typedef void(*fun_t)(int, std::string, uint16_t);
class Task
{
public:
  Task() {}
  Task(int sock, std::string client_ip, int client_port, fun_t handler) : _server_socket_fd(sock)
    , _client_ip(client_ip), _client_port(client_port), _handler(handler) {}
  //任务处理函数
  void operator()(const std::string& name) {
        _handler(_server_socket_fd, _client_ip, _client_port);
  }
private:
  int _server_socket_fd;
  std::string _client_ip;
  uint16_t _client_port;
    fun_t _handler;
};

实际可以让服务器处理不同的任务,当前服务器只是在进行字符串的回显处理,而实际要怎么处理这个任务完全是由任务类中的_handler成员来决定的

若想要让服务器处理其他任务,只需要修改()的重载函数就行了,而服务器的初始化、启动服务器以及线程池的代码都是不需要更改的,这被称为把通信功能和业务逻辑在软件上做解耦

网络测试

while :; do ps -aL|head -1&&ps -aL|grep tcp_server;echo "####################";sleep 1;done

运行服务端后,就算没有客户端发来连接请求,此时在服务端就已经有了11个线程,其中有一个是接收新连接的服务线程,而其余的5个是线程池中为客户端提供服务的线程

7b12a6e0bfed458fa15b3a999055e6d3.png

当客户端连接服务器后,服务端的主线程就会获取该客户端的连接请求,并将其封装为一个任务对象后放入任务队列,此时线程池中的10个线程就会有一个线程从任务队列当中获取到该任务,并执行该任务的处理函数为客户端提供服务

1f439088179f4cefade27f2cf29d3206.png

当第二个客户端发起连接请求时,服务端也会将其封装为一个任务类放入任务队列,然后线程池中的线程再从任务队列中获取到该任务进行处理,此时也是不同的执行流为这两个客户端提供的服务,因此这两个客户端是能够同时享受服务的

22f4731827ca4223a0a011c6682b215c.png

无论有多少客户端发来请求,在服务端都只会有线程池中的10个线程为之提供服务,线程池中的线程个数不会随着客户端连接的增多而增多,这些线程也不会因为客户端的退出而退出

五、地址转换函数

5.1 字符串IP转整数IP

inet_aton函数

int inet_aton(const char *cp, struct in_addr *inp);
  • cp:待转换的字符串IP。
  • inp:转换后的整数IP,输出型参数

返回值:若转换成功则返回一个非零值,若输入的地址不正确则返回零值

inet_addr函数

in_addr_t inet_addr(const char *cp);

参数cp:待转换的字符串IP

返回值:若输入地址有效,则返回转换后的整数IP;若无效,则返回INADDR_NONE(-1)

inet_pton函数

int inet_pton(int af, const char *src, void *dst);
  • af参数:协议家族
  • src参数:待转换的字符串IP
  • dst参数:转换后的整数IP,输出型参数

返回值说明:

  • 若转换成功,则返回1
  • 若输入的字符串IP无效,则返回0
  • 若输入的协议家族af无效,则返回-1,并将errno设置为EAFNOSUPPORT

5.2 整数IP转字符串IP

inet_ntoa函数

char *inet_ntoa(struct in_addr in);

参数in:待转换的整数IP

返回值:返回转换后的字符串IP

inet_ntop函数

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af参数:协议家族
  • src参数:待转换的整数IP
  • dst参数:转换后的字符串IP,输出型参数
  • size参数:用于指明dst中可用的字节数

返回值:若转换成功,则返回一个指向dst的非空指针;若转换失败,则返回NULL。

注意

  • 最常用的两个转换函数是inet_addr和inet_ntoa,因为这两个函数足够简单。这两个函数的参数就是需要转换的字符串IP或整数IP,而这两个函数的返回值就是对应的整数IP和字符串IP
  • 其中inet_pton和inet_ntop函数不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此这两个函数中对应的参数类型是void*
  • 转换函数都是为了满足某些打印场景的或者做某些数据分析,如网络安全方面的数据分析

5.3 inet_ntoa函数问题

inet_ntoa函数可以将4字节的整数IP转换成字符串IP,其中该函数返回的这个转换后的字符串IP是存储在静态存储区的,不需要调用者手动进行释放。若多次调用inet_ntoa函数,此时就会出现数据覆盖的问题

0ba0300c7ed74a10a4400053a53cb475.png

inet_ntoa函数内部只在静态存储区申请了一块区域,导致inet_ntoa函数第二次转换的结果就会覆盖第一次转换的结果

6522dbfe68d649b09b679ef3ea090cdb.png

若要多次调用inet_ntoa函数,那么就要及时保存inet_ntoa的转换结果

并发场景下的inet_ntoa函数

inet_ntoa函数内部只在静态存储区申请了一块区域,用于存储转换后的字符串IP,那么在线程场景下这块区域就叫做临界区,多线程在不加锁的情况下同时访问临界区必然会出现异常情况。并且在APUE中,也明确提出inet_ntoa不是线程安全的函数

#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
void* Func1(void* arg)
{
  struct sockaddr_in* p = (struct sockaddr_in*)arg;
  while (1){
    char* ptr1 = inet_ntoa(p->sin_addr);
    std::cout << "ptr1: " << ptr1 << std::endl;
    sleep(1);
  }
}
void* Func2(void* arg)
{
  struct sockaddr_in* p = (struct sockaddr_in*)arg;
  while (1){
    char* ptr2 = inet_ntoa(p->sin_addr);
    std::cout << "ptr2: " << ptr2 << std::endl;
        sleep(1);
  }
}
int main()
{
  struct sockaddr_in addr1;
  struct sockaddr_in addr2;
  addr1.sin_addr.s_addr = 0;
  addr2.sin_addr.s_addr = 0xffffffff;
  pthread_t tid1 = 0;
  pthread_create(&tid1, nullptr, Func1, &addr1);
    sleep(1);
  pthread_t tid2 = 0;
  pthread_create(&tid2, nullptr, Func2, &addr2);
  pthread_join(tid1, nullptr);
  pthread_join(tid2, nullptr);
  return 0;
}

但是实际在centos7上测试时,在多线程场景下调用inet_ntoa函数并没有出现问题,可能是该函数内部的实现加了互斥锁,这就跟接口本身的设计也是有关系的

4b4d870819654b0d82da78b7f2a75620.png

在多线程环境下更加推荐使用inet_ntop函数进行转换,因为该函数是由调用者自己提供缓冲区保存转换结果的,可以规避线程安全的问题

目录
相关文章
|
12月前
|
开发者 Python
Python Socket编程:不只是基础,更有进阶秘籍,让你的网络应用飞起来!
在数字时代,网络应用成为连接世界的桥梁。Python凭借简洁的语法和丰富的库支持,成为开发高效网络应用的首选。本文通过实时聊天室案例,介绍Python Socket编程的基础与进阶技巧。基础篇涵盖服务器和客户端的建立与数据交换;进阶篇则探讨多线程与异步IO优化方案,助力提升应用性能。通过本案例,你将掌握Socket编程的核心技能,推动网络应用飞得更高、更远。
182 1
|
10月前
|
Kubernetes 网络协议 Python
Python网络编程:从Socket到Web应用
在信息时代,网络编程是软件开发的重要组成部分。Python作为多用途编程语言,提供了从Socket编程到Web应用开发的强大支持。本文将从基础的Socket编程入手,逐步深入到复杂的Web应用开发,涵盖Flask、Django等框架的应用,以及异步Web编程和微服务架构。通过本文,读者将全面了解Python在网络编程领域的应用。
151 1
|
11月前
|
消息中间件 监控 网络协议
Python中的Socket魔法:如何利用socket模块构建强大的网络通信
本文介绍了Python的`socket`模块,讲解了其基本概念、语法和使用方法。通过简单的TCP服务器和客户端示例,展示了如何创建、绑定、监听、接受连接及发送/接收数据。进一步探讨了多用户聊天室的实现,并介绍了非阻塞IO和多路复用技术以提高并发处理能力。最后,讨论了`socket`模块在现代网络编程中的应用及其与其他通信方式的关系。
863 3
|
11月前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【10月更文挑战第10天】网络协议定义了机器间通信的标准格式,确保信息准确无损地传输。主要分为两种模型:OSI七层模型与TCP/IP模型。
|
12月前
|
网络协议 Python
告别网络编程迷雾!Python Socket编程基础与实战,让你秒变网络达人!
在网络编程的世界里,Socket编程是连接数据与服务的关键桥梁。对于初学者,这往往是最棘手的部分。本文将用Python带你轻松入门Socket编程,从创建TCP服务器与客户端的基础搭建,到处理并发连接的实战技巧,逐步揭开网络编程的神秘面纱。通过具体的代码示例,我们将掌握Socket的基本概念与操作,让你成为网络编程的高手。无论是简单的数据传输还是复杂的并发处理,Python都能助你一臂之力。希望这篇文章成为你网络编程旅程的良好开端。
137 3
|
12月前
|
网络协议 Python
网络世界的建筑师:Python Socket编程基础与进阶,构建你的网络帝国!
在数字宇宙中,网络如同复杂脉络连接每个角落,Python Socket编程则是开启这一世界的钥匙。本文将引导你从基础概念入手,逐步掌握Socket编程,并通过实战示例构建TCP/UDP服务器与客户端。你将学会使用Python的socket模块进行网络通信,了解TCP与UDP的区别,并运用多线程与异步IO提升服务器性能。跟随本文指引,成为网络世界的建筑师,构建自己的网络帝国。
108 2
|
12月前
|
网络协议 开发者 Python
网络编程小白秒变大咖!Python Socket基础与进阶教程,轻松上手无压力!
在网络技术飞速发展的今天,掌握网络编程已成为开发者的重要技能。本文以Python为工具,带你从Socket编程基础逐步深入至进阶领域。首先介绍Socket的概念及TCP/UDP协议,接着演示如何用Python创建、绑定、监听Socket,实现数据收发;最后通过构建简单的聊天服务器,巩固所学知识。让初学者也能迅速上手,成为网络编程高手。
191 1
|
网络协议 安全 网络安全
震惊!Python Socket竟能如此玩转网络通信,基础到进阶全攻略!
【9月更文挑战第12天】在网络通信中,Socket编程是连接不同应用与服务的基石。本文通过问答形式,从基础到进阶全面解析Python Socket编程。涵盖Socket的重要性、创建TCP服务器与客户端、处理并发连接及进阶话题如非阻塞Socket、IO多路复用等,帮助读者深入了解并掌握网络通信的核心技术。
363 6
|
消息中间件 网络协议 网络安全
解锁Python Socket新姿势,进阶篇带你玩转高级网络通信技巧!
【9月更文挑战第13天】在掌握了Python Socket编程基础后,你是否想进一步提升技能?本指南将深入探讨Socket编程精髓,包括从阻塞到非阻塞I/O以提高并发性能,使用`select`进行非阻塞操作示例;通过SSL/TLS加密通信保障数据安全,附带创建SSL服务器的代码实例;以及介绍高级网络协议与框架,如HTTP、WebSocket和ZeroMQ,帮助你简化复杂应用开发。通过学习这些高级技巧,你将在网络编程领域更进一步。
122 3
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【9月更文挑战第14天】网络协议是机器间交流的约定格式,确保信息准确传达。主要模型有OSI七层与TCP/IP模型,通过分层简化复杂网络环境。IP地址全局定位设备,MAC地址则在本地网络中定位。网络分层后,数据包层层封装,经由不同层次协议处理,最终通过Socket系统调用在应用层解析和响应。

热门文章

最新文章