liunx网络套接字 | 实现基于tcp协议的echo服务

        前言:本节讲述linux网络下的tcp协议套接字相关内容。博主以实现tcp服务为主线,穿插一些小知识点。以先粗略实现,后精雕细琢为思路讲述实现服务的过程。下面开始我们的学习吧。

        ps:本节内容建议了解网络端口号的友友们观看哦。

目录

实现内容

线程池版本整体代码

准备文件

makefile

tcpserver.hpp

main.cc

tcpclient

version1运行结果

version2版本

version3版本

version4版本


实现内容

        本篇内容将要实现一个服务端, 一个客户端。 然后客户端用来链接服务端, 向服务端发送消息, 然后服务端能够接收到消息并将消息返回给客户端。 

        实现的版本有四个:

  •         version1:实现单执行流的客户服务echo服务, 就是服务端只为一个服务端进行服务。 
  •         version2:在version1的版本上, 添加进程, 实现多进程的客户服务echo服务, 就是服务端为多个客户端进行服务, 但是因为是多进程,所以开销大。
  •         version3:改进version2版本, 将多进程改成多线程。实现多线程的echo服务。 但是当用户很多的时候, 线程量太大, 无法控制。
  •         version4:终极版本, 改进version3, 以线程池为基础, 实现可控的多线程echo服务。 控制线程个数, 既保证了并发性, 又防止了用户太多,线程爆满的问题。

        博主先实现version1, 然后在version1的基础上进行改版。下面开始实现: 

线程池版本整体代码

tcpserver


#pragma once
#include "Log.hpp"
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "ThreadPool.h"
#include "Task.h"
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

const int defaultfd = -1;
const int defaultport = 8080;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 直接用,一般不要设置的太大。


class TcpServer;  //声明

class ThreadData
{
public:
    ThreadData(int fd, string ip, uint16_t port, TcpServer* const t)
        : sockfd_(fd), clientip_(ip), clientport_(port), t_(t)
    {
    }

public:
    int sockfd_;
    string clientip_;
    uint16_t clientport_;
public:
    TcpServer* const t_;
};



Log lg;

enum
{
    SockError = 2,
    BindError,
    ListenError
};

class TcpServer
{
public:
    TcpServer(int port = defaultport, string ip = defaultip, int sockfd = defaultfd)
        : listensockfd_(sockfd), ip_(ip), port_(port)
    {
    }

    void InitServer()
    {
        listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensockfd_ < 0)
        {
            lg(Fatal, "create socket, errno: %d, strerror: %s", errno, strerror(errno));
            exit(SockError);
        }
        //
        lg(Info, "create socket success, sockfd: %d", listensockfd_);

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &(local.sin_addr)); // 主机序列转网络学列。 inet_aton是一个线程安全的函数。

        // 绑定
        if (bind(listensockfd_, (sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BindError);
        }
        // tcp面向字节流, 是被动的, 所以要将对应的socket设置为监听状态。
        if (listen(listensockfd_, backlog) < 0) // backlock表示的是底层全连接队列的长度。 这个参数对意思, 不做解释。
        {
            lg(Fatal, "Listen error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(ListenError);
        }
        lg(Info, "Listen has success");
    }



    void Start()
    {
        ThreadPool<Task>::GetInstance()->Start();
        lg(Info, "tcpServer is running...");
        for (;;) // tcp协议也是一种一直处于运行的服务
        {
            // tcp是面向连接的, 所以他比udp还多了一步accept, 先将客户端与服务端连接起来。accept的返回值成功返回整数文件描述符,否则-1被返回, 错误码被设置
            // 1、获取新连接,
            struct sockaddr_in client; // 获取的是客户端的addr
            socklen_t len = sizeof(client);

            int sockfd = accept(listensockfd_, (sockaddr *)&client, &len); // accept成功, 就能知道是谁连接的我。
            if (sockfd < 0)                                                // 关于这两个套接字, sockfd_的核心工作就只是把链接获取上来, 未来进行IO操作, 看的是sockfd。
            {
                lg(Waring, "listen error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port); // 网络序列转主机序列
            char clientip[32];
            inet_ntop(sockfd, &client.sin_addr, clientip, sizeof(clientip));
            // 2、根据新连接进行通信
            lg(Info, "get a new link..., sockfd:%d, clientport: %d, clientip: %s", sockfd, clientport, clientip);

  

            //version--4线程池版本
            Task task_(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(task_);
        }
    }

    ~TcpServer()
    {
    }

private:
    int listensockfd_; // 监听套接字, 只用来升起服务器, 接收链接

    uint16_t port_;
    string ip_;
};

 main.cc

#include"tcpserver.hpp"
#include<memory>

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        cout << "has return" << endl;
        return 1;
    }
    //
    uint16_t port = stoi(argv[1]);

    unique_ptr<TcpServer> tcpsvr(new TcpServer(port));
    tcpsvr->InitServer();
    tcpsvr->Start();
    
    return 0;
}

 tcpclient

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

int main(int argc, char* argv[])
{    
    //处理argc, argv[]
    if (argc != 3)
    {
        cout << "has return " << endl;
        return 1;
    }
    //
    uint16_t serverport = stoi(argv[2]);
    string serverip = argv[1];
    
    //创建addr结构体, 设置端口号ip地址
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        cout << "socket error" << endl;
        return 1;
    }
    sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    //1、客户端要绑定端口号, 但是不需要显示的绑定, 而是系统进行随机端口的绑定。 
    int n = connect(sockfd,(sockaddr*)&server, sizeof(server));
    if (n < 0) 
    {
        cerr << "connect error..." << endl;
        return 2;
    }


    //2、发送信息, 接收信息。
    string message;
    while (true)
    {
        cout << "Please Enter# ";
        getline(cin, message);

        //
        write(sockfd, message.c_str(), message.size());

        char inbuffer[4096];
        int n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            cout << inbuffer << endl;
        }
    }
    
    return 0;
}

ThreadPool


#pragma once
#include<iostream>
#include<pthread.h>
#include<vector>
#include<string>
using namespace std;
#include<queue>
#include<ctime>
#include<unistd.h>

//对线程的属性做一下封装, 有利于线程池的保存以及后面的处理
struct ThreadInfo
{
    pthread_t tid_;
    string name_;   
};


template<class T>
class ThreadPool
{
    static const int defaultnum = 5;  //默认的线程池的大小(线程池的大小就是里面包含的线程的数量)

private:
    //加锁解锁
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }

    //唤醒线程, 线程是可以被挂起的(就比如信号量)。 当任务没有的时候,线程就要被挂起, 有任务后再唤醒
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }

    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }

    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }



public:

    //线程要执行的函数
    static void* HandlerTask(void* args)
    {
        ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
        while(true) 
        {
            tp->Lock();
            while (tp->tasks_.empty())
            {
                tp->ThreadSleep(); //如果队列里面没有任务了, 就让线程去休眠。
            }
            //否则就去拿到tasks里面的任务
            T t = tp->tasks_.front();
            tp->tasks_.pop();
            
            //
            tp->Unlock();
            t();  //每一个线程先对任务进行消费, 消费完成之后处理任务。    

        }
    }

    //运行这个线程池, 也就是先将线程创建出来。 然后去运行线程
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name_ = "thread-";
            threads_[i].name_ += to_string(i);   
            pthread_create(&(threads_[i].tid_), nullptr, HandlerTask, this);
        }
    }

    //主线程给线程池发送任务, 注意, 这个任务一定是可以被储存起来的。 因为当我们的任务很多很多的时候, 我们的线程池内的线程要一个一个地对这些任务进行处理
    void Push(const T& t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();

        Unlock();
    }

    //获取单例
    //改编成单例的步骤里面只有这里要说一下, 就是为什么我们要套双层判断。 其实这里的最外面的一层的判断是我们另外加上去的。 为什么
    //要加这个判断呢? 就是如果我们不加最外层这一层判断。 那么每一个线程获取单例都要申请所,加锁。 不就是相当于所有的线程都在串行执行? 这就有效率问题。 
    //解决方案就是这个再加一层判断。 这样假如有四个线程。 那么一开始四个线程都在判断, 那么它们四个线程都进入了if里面。 然后就都申请锁, 但是只有第一个线程能够
    //进入第二层里面, 其他的进入不了。 那么当这一轮的四个线程都申请一次锁候就都退出了函数, 然后就都去做自己的事情了。 问题是, 当下次它们再来申请单例对象的时候它们连
    //第一层判断都成功不了了, 也就都不用加锁解锁了, 这就大大提高了效率!!!
    static ThreadPool<T>* GetInstance(int num = defaultnum)
    {
        if (tp_ == nullptr)
        {
            pthread_mutex_lock(&tp_->lock_);
            
            if (tp_ == nullptr) 
            {
                tp_ = new ThreadPool<T>(num);
            }

            pthread_mutex_unlock(&tp_->lock_);
        }
        
        return tp_;
    }



private:
    //构造函数私有化, 只有Getinstance里面才能创建。 
    ThreadPool(int num = defaultnum)
        :threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }

    //单例模式只有一个对象, 所以要将拷贝构造和拷贝赋值封住, 为了防止有人在外部重新拷贝一个对象。 
    ThreadPool(const ThreadPool<T>& tp) = delete;
    const ThreadPool<T>& operator=(const ThreadPool<T>& tp) = delete;
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }



private:
    vector<ThreadInfo> threads_;   //线程都维护在vector当中, 这个就是线程池里面的线程的个数,
    queue<T> tasks_ ;              //向线程池中发送任务, 这个队列里面保存的就是我们的任务的数目。 

    pthread_mutex_t mutex_;        //锁,用来生产者线程(本份代码只是主线程)给线程池发送任务时候加锁使用以及消费者线程抢夺任务时加锁使用 

    pthread_cond_t cond_;          //条件变量, 用来没有任务的时候,消费者要挂起。 

    

    static pthread_mutex_t lock_;         //锁, 这个锁是为了在获取单例的时候能够让线程原子性的访问if (tp_ == nullptr)。
    static ThreadPool<T>* tp_;    //tp指针, 这就是唯一个单例对象。 

};

template<class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

Task

#include <iostream>
using namespace std;
#include"Log.hpp"
#include <vector>
#include <string>
extern Log lg;

// Task.h文件里面包含了任务类, 这个是我们线程池要执行的任务
class Task
{
public:
    // 构造函数, 第一个参数data1, 第二个参数data2, 第三个参数是加减乘除的符号。 这个任务就是进行四则运算
    Task(int sockfd, string clientip, uint16_t clientport)
        :sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
    {
    }

    ~Task() {}

    // 执行任务的接口run(), 这个方法对三个变量进行判断, 然后进行运算。
    void run()
    {
        char buffer[4096];
        string temp = clientip_;
        while (true)
        {
            ssize_t n = read(sockfd_, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                cout << "client say#: " << buffer << endl;
                string echo_string("tcpserver echo#  " + (string)buffer);

                write(sockfd_, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                lg(Info, "quit sockfd:%d ", sockfd_);
                exit(1);
            }
            else
            {
                lg(Waring, "Waring, sockfd:%d, clientport: %d, clientip: %s", sockfd_, clientport_, temp.c_str());
            }
            //
        }
    }

    // 仿函数, 为了方便我们的对象能够像函数一样使用。
    void operator()()
    {
        run();
    }

private:
    // 每一个任务对象里面都有三个参数, 一个data1, 一个data2, 最后一个op_
    int sockfd_;
    string clientip_;
    uint16_t clientport_;
};
    

 

准备文件

先准备好文件。 tcpserver.hpp实现服务的接口, main.cc运行服务端。 然后tcpclient.cc运行客户端。 

 

makefile

makefile不解释, 直接上代码(这里加上-g是为了后续方便调试, 也可以不加)

.PHONY:all
all: tcpserver.exe tcpclient.exe

tcpserver.exe:main.cc
	g++ -o $@ $^ -std=c++11 -lpthread -g
tcpclient.exe:tcpclient.cc
	g++ -o $@ $^ -std=c++11 -lpthread -g

.PHONY:clean
clean:
	rm -rf tcpserver.exe tcpclient.exe

 tcpserver.hpp

先来看框架

#pragma once
#include "Log.hpp"
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include"ThreadPool.h"
#include"Task.h"
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

const int defaultfd = -1;
const int defaultport = 8080;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 直接用,一般不要设置的太大。

Log lg;

enum
{
    SockError = 2,
    BindError,
    ListenError
};

class TcpServer
{
public:
    //构造函数, 将服务端的端口号, ip地址传过来
    TcpServer(int port = defaultport, string ip = defaultip, int sockfd = defaultfd)
        : listensockfd_(sockfd), ip_(ip), port_(port)
    {}

    //初始化服务端,分两步:绑定和监听
    void InitServer()
    {
        //绑定
        
        //监听
    }

    
    //运行服务端
    void Start()
    {

    }
    

    //执行相应的服务
    void Service(int sockfd, const string &clientip, uint16_t clientport)
    {

    } 
    
    //析构函数
    ~TcpServer()
    {}

private:
    int listensockfd_; // 监听套接字, 只用来升起服务器, 接收链接

    uint16_t port_;
    string ip_;
};

然后初始化就是创建sockaddr结构体, 创建套接字, 然后绑定。 因为tcp是面向字节流的。 所以还要对网卡进行监听。下面是初始化服务。


    //对服务端进行初始化
    void InitServer()
    {
        listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensockfd_ < 0)
        {
            lg(Fatal, "create socket, errno: %d, strerror: %s", errno, strerror(errno));
            exit(SockError);
        }
        //
        lg(Info, "create socket success, sockfd: %d", listensockfd_);

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &(local.sin_addr)); // 主机序列转网络学列。 inet_aton是一个线程安全的函数。

        // 绑定
        if (bind(listensockfd_, (sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BindError);
        }
        // tcp面向字节流, 是被动的, 所以要将对应的socket设置为监听状态。
        if (listen(listensockfd_, backlog) < 0) // backlock表示的是底层全连接队列的长度。 这个参数对意思, 不做解释。
        {
            lg(Fatal, "Listen error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(ListenError);
        }
        lg(Info, "Listen has success");
    }

        然后是运行服务, 运行服务也是分两步: accept与客户端建立连接。 然后执行服务。

    void Start()
    {

        lg(Info, "tcpServer is running...");
        for (;;) // tcp协议也是一种一直处于运行的服务
        {
            // tcp是面向连接的, 所以他比udp还多了一步accept, 先将客户端与服务端连接起来。accept的返回值成功返回整数文件描述符,否则-1被返回, 错误码被设置
            // 1、获取新连接,
            struct sockaddr_in client; // 获取的是客户端的addr
            socklen_t len = sizeof(client);

            int sockfd = accept(listensockfd_, (sockaddr *)&client, &len); // accept成功, 就能知道是谁连接的我。
            if (sockfd < 0)                                                // 关于这两个套接字, sockfd_的核心工作就只是把链接获取上来, 未来进行IO操作, 看的是sockfd。
            {
                lg(Waring, "listen error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port); // 网络序列转主机序列
            char clientip[32];
            inet_ntop(sockfd, &client.sin_addr, clientip, sizeof(clientip));
            // 2、根据新连接进行通信
            lg(Info, "get a new link..., sockfd:%d, clientport: %d, clientip: %s", sockfd, clientport, clientip);

            //version--1 单进程版
            Service(sockfd, clientip, clientport);
            close(sockfd);

        }
    }

        然后执行的服务是echo服务, 就是先接受客户端发来的信息, 然后将信息加工一下发回去。


    void Service(int sockfd, const string &clientip, uint16_t clientport)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                cout << "client say#: " << buffer << endl;
                string message("tcpserver echo#  " + string(buffer));
                //
                write(sockfd, message.c_str(), message.size());
            }
            else if (n == 0)
            {
                lg(Info, "quit sockfd:%d ", sockfd);
                exit(1);
            }
            else
            {
                lg(Waring, "Waring, sockfd:%d, clientport: %d, clientip: %s", sockfd, clientport, clientip.c_str());
            }
        }
    }

         上面就是整个的代码。

        我们这里说一下accept这个代码 

        accept代码的第一个参数是sockfd, 就是网卡的文件fd。 然后第二个参数和第三个参数都是输出型参数。 能够将对方也就是客户端的sockaddr带出来。 

main.cc

        主函数就是接收到传来的端口号, 创建服务端然后初始化并运行起来。

#include"tcpserver.hpp"
#include<memory>

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        cout << "has return" << endl;
        return 1;
    }
    //
    uint16_t port = stoi(argv[1]);

    unique_ptr<TcpServer> tcpsvr(new TcpServer(port));
    tcpsvr->InitServer();
    tcpsvr->Start();
    
    return 0;
}   

 tcpclient

        先看客户端的框架, 就是先链接服务端。 然后就给服务端发信息,接收信息。 (接收到的这个信息是被服务端处理过的)

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

int main(int argc, char* argv[])
{
    //处理argc, argv[]
    
    //先连接


    //再发数据接受数据
    
    return 0;
}

处理argc的时候, 因为我们的参数一定是三个。即, 一个程序名一个端口号, 一个IP地址。 所以如果argc不是等于3的话直接返回。 为3的话就将端口号以及ip地址保存一下。 其中ip地址是要连接到的服务端的ip地址, 端口号是要连接到服务端的端口号。

    //处理argc, argv[]
    if (argc != 3)
    {
        cout << "has return " << endl;
        return 1;
    }
    //
    uint16_t serverport = stoi(argv[2]);
    string serverip = argv[1];

然后创建addr结构,连接到服务端。 

    //创建addr结构体, 设置端口号ip地址
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        cout << "socket error" << endl;
        return 1;
    }
    sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    //1、客户端要绑定端口号, 但是不需要显示的绑定, 而是系统进行随机端口的绑定。 
    int n = connect(sockfd,(sockaddr*)&server, sizeof(server));
    if (n < 0) 
    {
        cerr << "connect error..." << endl;
        return 2;
    }

最后就是收发消息, 这里创建循环, 让我们可以执行多次服务, 多次收发消息。

    //2、发送信息, 接收信息。
    string message;
    while (true)
    {
        cout << "Please Enter# ";
        getline(cin, message);

        //
        write(sockfd, message.c_str(), message.size());

        char inbuffer[4096];
        int n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            cout << inbuffer << endl;
        }
    }
    

然后我们就能够运行了。下面是运行结果。

version1运行结果

可以看到已经可以收发消息。

version2版本

        version2版本是创建多进程。 但是, 要知道, 我们的父进程创建子进程后要进行等待。 否则会造成内存泄漏。 但是我们父进程等待, 父进程又不能向下运行代码了, 就不能继续创建子进程了。 所以, 为了解决这个问题。 我们就可以创建一个孤儿进程。 让子进程创建好孙子进程后直接退出, 将孙子进程托孤。 父进程等待子进程后就继续向下执行。 这样就能创建一批孙子进程并发访问!!!


    void Start()
    {

        lg(Info, "tcpServer is running...");
        for (;;) // tcp协议也是一种一直处于运行的服务
        {
            // tcp是面向连接的, 所以他比udp还多了一步accept, 先将客户端与服务端连接起来。accept的返回值成功返回整数文件描述符,否则-1被返回, 错误码被设置
            // 1、获取新连接,
            struct sockaddr_in client; // 获取的是客户端的addr
            socklen_t len = sizeof(client);

            int sockfd = accept(listensockfd_, (sockaddr *)&client, &len); // accept成功, 就能知道是谁连接的我。
            if (sockfd < 0)                                                // 关于这两个套接字, sockfd_的核心工作就只是把链接获取上来, 未来进行IO操作, 看的是sockfd。
            {
                lg(Waring, "listen error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port); // 网络序列转主机序列
            char clientip[32];
            inet_ntop(sockfd, &client.sin_addr, clientip, sizeof(clientip));
            // 2、根据新连接进行通信
            lg(Info, "get a new link..., sockfd:%d, clientport: %d, clientip: %s", sockfd, clientport, clientip);

            // // version--1 单进程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);

            //version--2 多进程版
            pid_t id = fork();
            if (id == 0)
            {
                close(listensockfd_);
                if (fork() > 0)
                {
                    exit(0);
                }
                Service(sockfd, clientip, clientport);
                close(sockfd);
                exit(0);
                //child
            }
            close(sockfd);
            pid_t rid = waitpid(id, nullptr, 0);
            
        }
    }

 version3版本

        第三版本是多线程版本, 多线程同样有主线程等待的问题,我们的主线程一旦要等待子线程, 那么就不能向后执行了。 所以为了能够并发, 就要对子线程进行分离。 

        下面是代码的改动:

        第一个改动地方就是创建一个ThreadData类:


class TcpServer;  //声明

//创建这个类是为了能够将服务端对象传给线程去执行
class ThreadData
{
public:
    ThreadData(int fd, string ip, uint16_t port, TcpServer* const t)
        : sockfd_(fd), clientip_(ip), clientport_(port), t_(t)
    {
    }

public:
    int sockfd_;
    string clientip_;
    uint16_t clientport_;
public:
    TcpServer* const t_;
};

        然后第二个改动的地方就是线程要执行的动作:


    static void *pthrun(void *args)
    {
        pthread_detach(pthread_self()); // 子线程直接分离
        // 一个进程打开的所有的文件描述符表, 其他进程能看到呢?
        ThreadData* td = static_cast<ThreadData*>(args);
        td->t_->Service(td->sockfd_, td->clientip_, td->clientport_);

    }

        第三个改动就是start函数里面执行服务的代码部分:

    void Start()
    {

        lg(Info, "tcpServer is running...");
        for (;;) // tcp协议也是一种一直处于运行的服务
        {
            // tcp是面向连接的, 所以他比udp还多了一步accept, 先将客户端与服务端连接起来。accept的返回值成功返回整数文件描述符,否则-1被返回, 错误码被设置
            // 1、获取新连接,
            struct sockaddr_in client; // 获取的是客户端的addr
            socklen_t len = sizeof(client);

            int sockfd = accept(listensockfd_, (sockaddr *)&client, &len); // accept成功, 就能知道是谁连接的我。
            if (sockfd < 0)                                                // 关于这两个套接字, sockfd_的核心工作就只是把链接获取上来, 未来进行IO操作, 看的是sockfd。
            {
                lg(Waring, "listen error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port); // 网络序列转主机序列
            char clientip[32];
            inet_ntop(sockfd, &client.sin_addr, clientip, sizeof(clientip));
            // 2、根据新连接进行通信
            lg(Info, "get a new link..., sockfd:%d, clientport: %d, clientip: %s", sockfd, clientport, clientip);

            // // version--1 单进程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);

            //version--2 多进程版
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     close(listensockfd_);
            //     if (fork() > 0)
            //     {
            //         exit(0);
            //     }
            //     Service(sockfd, clientip, clientport);
            //     close(sockfd);
            //     exit(0);
            //     //child
            // }
            // close(sockfd);
            // pid_t rid = waitpid(id, nullptr, 0);

            // version--3多线程版本
            ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, pthrun, td);
            
        }
    }

运行结果

一开始一个server服务, 然后我们添加一个client就增加一个server服务, 增加一个client就增加一个服务。

version4版本

        version4线程池版本, version4的线程池版本就是我们创建好线程池。 然后创建一个任务类, 将我们要执行的服务作为任务类的一个结构。 这就要求我们的这个任务类里面必须有我们的ip, port, sockfd这样的。 字段。 并且我们还要引入几个头文件, 下面为代码:

首先要有两个新文件, 一个包含task类, 一个包含线程池类。

我们让task类里面包含服务的方法。就是run方法。


#include <iostream>
using namespace std;
#include"Log.hpp"
#include <vector>
#include <string>
extern Log lg;

// Task.h文件里面包含了任务类, 这个是我们线程池要执行的任务
class Task
{
public:
    // 构造函数, 第一个参数data1, 第二个参数data2, 第三个参数是加减乘除的符号。 这个任务就是进行四则运算
    Task(int sockfd, string clientip, uint16_t clientport)
        :sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
    {
    }

    ~Task() {}

    // 执行任务的接口run(), 这个方法对三个变量进行判断, 然后进行运算。
    void run()
    {
        char buffer[4096];
        string temp = clientip_;
        while (true)
        {
            ssize_t n = read(sockfd_, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                cout << "client say#: " << buffer << endl;
                string echo_string("tcpserver echo#  " + (string)buffer);

                write(sockfd_, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                lg(Info, "quit sockfd:%d ", sockfd_);
                exit(1);
            }
            else
            {
                lg(Waring, "Waring, sockfd:%d, clientport: %d, clientip: %s", sockfd_, clientport_, temp.c_str());
            }
            //
        }
    }

    // 仿函数, 为了方便我们的对象能够像函数一样使用。
    void operator()()
    {
        run();
    }

private:
    // 每一个任务对象里面都有三个参数, 一个data1, 一个data2, 最后一个op_
    int sockfd_;
    string clientip_;
    uint16_t clientport_;
};

 然后线程池代友友们如果没写过可以自己实现一个,或者直接用博主的, 如下:


#pragma once
#include<iostream>
#include<pthread.h>
#include<vector>
#include<string>
using namespace std;
#include<queue>
#include<ctime>
#include<unistd.h>

//对线程的属性做一下封装, 有利于线程池的保存以及后面的处理
struct ThreadInfo
{
    pthread_t tid_;
    string name_;   
};

template<class T>
class ThreadPool
{
    static const int defaultnum = 5;  //默认的线程池的大小(线程池的大小就是里面包含的线程的数量)

private:
    //加锁解锁
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }

    //唤醒线程, 线程是可以被挂起的(就比如信号量)。 当任务没有的时候,线程就要被挂起, 有任务后再唤醒
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }

    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }

    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }

public:

    //线程要执行的函数
    static void* HandlerTask(void* args)
    {
        ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
        while(true) 
        {
            tp->Lock();
            while (tp->tasks_.empty())
            {
                tp->ThreadSleep(); //如果队列里面没有任务了, 就让线程去休眠。
            }
            //否则就去拿到tasks里面的任务
            T t = tp->tasks_.front();
            tp->tasks_.pop();
            
            //
            tp->Unlock();
            t();  //每一个线程先对任务进行消费, 消费完成之后处理任务。    

        }
    }

    //运行这个线程池, 也就是先将线程创建出来。 然后去运行线程
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name_ = "thread-";
            threads_[i].name_ += to_string(i);   
            pthread_create(&(threads_[i].tid_), nullptr, HandlerTask, this);
        }
    }

    //主线程给线程池发送任务, 注意, 这个任务一定是可以被储存起来的。 因为当我们的任务很多很多的时候, 我们的线程池内的线程要一个一个地对这些任务进行处理
    void Push(const T& t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();

        Unlock();
    }

    //获取单例
    //改编成单例的步骤里面只有这里要说一下, 就是为什么我们要套双层判断。 其实这里的最外面的一层的判断是我们另外加上去的。 为什么
    //要加这个判断呢? 就是如果我们不加最外层这一层判断。 那么每一个线程获取单例都要申请所,加锁。 不就是相当于所有的线程都在串行执行? 这就有效率问题。 
    //解决方案就是这个再加一层判断。 这样假如有四个线程。 那么一开始四个线程都在判断, 那么它们四个线程都进入了if里面。 然后就都申请锁, 但是只有第一个线程能够
    //进入第二层里面, 其他的进入不了。 那么当这一轮的四个线程都申请一次锁候就都退出了函数, 然后就都去做自己的事情了。 问题是, 当下次它们再来申请单例对象的时候它们连
    //第一层判断都成功不了了, 也就都不用加锁解锁了, 这就大大提高了效率!!!
    static ThreadPool<T>* GetInstance(int num = defaultnum)
    {
        if (tp_ == nullptr)
        {
            pthread_mutex_lock(&tp_->lock_);
            
            if (tp_ == nullptr) 
            {
                tp_ = new ThreadPool<T>(num);
            }

            pthread_mutex_unlock(&tp_->lock_);
        }
        
        return tp_;
    }

private:
    //构造函数私有化, 只有Getinstance里面才能创建。 
    ThreadPool(int num = defaultnum)
        :threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }

    //单例模式只有一个对象, 所以要将拷贝构造和拷贝赋值封住, 为了防止有人在外部重新拷贝一个对象。 
    ThreadPool(const ThreadPool<T>& tp) = delete;
    const ThreadPool<T>& operator=(const ThreadPool<T>& tp) = delete;
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

private:
    vector<ThreadInfo> threads_;   //线程都维护在vector当中, 这个就是线程池里面的线程的个数,
    queue<T> tasks_ ;              //向线程池中发送任务, 这个队列里面保存的就是我们的任务的数目。 

    pthread_mutex_t mutex_;        //锁,用来生产者线程(本份代码只是主线程)给线程池发送任务时候加锁使用以及消费者线程抢夺任务时加锁使用 

    pthread_cond_t cond_;          //条件变量, 用来没有任务的时候,消费者要挂起。 

    static pthread_mutex_t lock_;         //锁, 这个锁是为了在获取单例的时候能够让线程原子性的访问if (tp_ == nullptr)。
    static ThreadPool<T>* tp_;    //tp指针, 这就是唯一个单例对象。 

};

template<class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

        然后就是start运行函数


    void Start()
    {
        ThreadPool<Task>::GetInstance()->Start();
        lg(Info, "tcpServer is running...");
        for (;;) // tcp协议也是一种一直处于运行的服务
        {
            // tcp是面向连接的, 所以他比udp还多了一步accept, 先将客户端与服务端连接起来。accept的返回值成功返回整数文件描述符,否则-1被返回, 错误码被设置
            // 1、获取新连接,
            struct sockaddr_in client; // 获取的是客户端的addr
            socklen_t len = sizeof(client);

            int sockfd = accept(listensockfd_, (sockaddr *)&client, &len); // accept成功, 就能知道是谁连接的我。
            if (sockfd < 0)                                                // 关于这两个套接字, sockfd_的核心工作就只是把链接获取上来, 未来进行IO操作, 看的是sockfd。
            {
                lg(Waring, "listen error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port); // 网络序列转主机序列
            char clientip[32];
            inet_ntop(sockfd, &client.sin_addr, clientip, sizeof(clientip));
            // 2、根据新连接进行通信
            lg(Info, "get a new link..., sockfd:%d, clientport: %d, clientip: %s", sockfd, clientport, clientip);

            // // version--1 单进程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);

            //version--2 多进程版
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     close(listensockfd_);
            //     if (fork() > 0)
            //     {
            //         exit(0);
            //     }
            //     Service(sockfd, clientip, clientport);
            //     close(sockfd);
            //     exit(0);
            //     //child
            // }
            // close(sockfd);
            // pid_t rid = waitpid(id, nullptr, 0);
            

            // // version--3多线程版本
            // ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);
            // pthread_t tid;
            // pthread_create(&tid, nullptr, pthrun, td);

            //version--4线程池版本
            Task task_(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(task_);
        }
    }

 运行结果:

我们可以看到, 只要已启动服务端的瞬间, 就能创建出6个线程(一个主线程, 五个分线程)

 

 ——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!   

在Windows和Linux平台下,套接字编程的实现方式存在一些关键差异。这些差异主要体现在API的设计、文件描述符的使用、以及系统的多任务处理模型等方面。 ### Windows 平台下的套接字编程 Windows系统采用了WinSock API来实现套接字编程。这个API不仅包含了伯克利套接字API的功能,还扩展了一些额外的WSA函数,以适应Windows的协同多任务和事件驱动编程模型[^1]。这意味着,在Windows平台上,开发者可以利用更丰富的API来进行复杂的网络通信,并且能够更好地与Windows自身的特性相结合。 此外,由于Windows对socket和文件进行了区分,所以在进行套接字编程时不能直接使用文件I/O的相关函数,这与Linux平台的做法有所不同[^2]。 ### Linux 平台下的套接字编程 相比之下,Linux系统中的套接字被视为文件的一种形式,因此在网络传输过程中可以使用标准的文件I/O操作来处理套接字。这种设计简化了开发者的编程工作,使得网络编程更加直观和便捷[^2]。 Linux为不同类型的套接字提供了统一的socket API,根据不同的地址族和套接字类型实现了不同的网络协议和数据结构。应用程序只需遵循API规范来创建和使用套接字,即可实现进程间通信和网络通信等功能[^3]。 ### 两者之间的主要区别 - **API差异**:Linux支持传统的伯克利套接字API,而Windows除了支持这些API外,还提供了一系列扩展功能,如WSA系列函数。 - **文件描述符**:在Linux中,套接字作为特殊的文件处理,可以直接使用文件I/O函数;而在Windows中,套接字不是作为文件处理的,因此不能直接使用文件I/O函数。 - **多任务处理模型**:Windows的协同多任务和事件驱动编程模型要求特定的API支持,这影响了WinSock的设计;而Linux则倾向于使用更传统的多线程或多进程模型来处理并发任务。 综上所述,虽然两个平台都支持基本的套接字编程功能,但在具体实现细节上有着显著的不同。选择哪个平台进行开发通常取决于项目需求和个人偏好。 ```c // 示例代码展示如何在Linux中创建一个简单的TCP服务器 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> int main(void) { int server_fd, new_socket; struct sockaddr_in address; int addrlen = sizeof(address); // 创建监听套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置地址和端口 address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(8080); // 绑定套接字到指定的地址和端口 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 开始监听连接请求 if (listen(server_fd, 3) < 0) { perror("listen"); close(server_fd); exit(EXIT_FAILURE); } // 接受新的连接 if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept"); close(server_fd); exit(EXIT_FAILURE); } printf("Connection accepted\n"); // 关闭套接字 close(new_socket); close(server_fd); return 0; } ``` 这段C语言代码演示了一个非常基础的TCP服务器程序,它会在本地机器上的8080端口开始监听来自客户端的连接请求。
评论 190
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值