TCP通信

目录

一.服务端

1.1创建套接字与绑定

1.2监听

1.3服务端获取连接

 1.4服务端提供服务

二.客户端

2.1创建套接字

 2.2客户端获取连接

 2.3客户端发送需求

三.填充命令行参数

3.1客户端

3.2服务端

3.3结果测试

 四.线程版本服务端

4.1线程版

4.2线程池版


一.服务端

与上文介绍的UDP网络通信大概类似,部分函数的接口相同。

1.1创建套接字与绑定

class ServerTcp
{
public:
    ServerTcp(uint16_t port, std::string ip = " ")
        : ip_(ip), port_(port), listenSock_(-1)
    {
    }

    void init()
    {
        listenSock_ = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
        if (listenSock_ < 0)
        {
            std::cerr << "创建套接字失败\n";
            exit(-1);
        }
        std::cout << "创建套接字成功"
                  << " "
                  << "listenSock_="
                  << listenSock_ << std::endl;

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), 
        &local.sin_addr));
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) // 绑定
        {
            std::cerr << "绑定失败\n";
            exit(-2);
        }
        std::cout << "绑定成功" << std::endl;

private:
    int listenSock_;
    std::string ip_;
    uint16_t port_;
};

补充内容:

1.如果用的是云服务器,在填充服务器的IP地址时,不需要显示绑定IP地址,直接将IP地址设置为INADDR_ANY即可,此时服务器就可以从本地任何一张网卡当中读取数据。

2.创建套接字时所需的服务类型应该是SOCK_STREAM,在编写TCP网络通信时,SOCK_STREAM提供的是一个有序的、可靠的、全双工的、基于连接的流式服务。

1.2监听

TCP服务器是面向连接的,客户端向服务器发送数据时,要确保二者已经建立了关联,将套接字设置为监听状态,然后去监听socket。

函数listen:

int listen(int sockfd, int backlog);

参数解释:

sockfd:需要设置为监听状态的套接字对应的文件描述符。

backlog:全连接队列的最大长度。若有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度。

返回值说明:成功返回0,失败返回-1。

  if (listen(listenSock_, 5 ) < 0)
   {
      exit(-2);
   }
  std::cout << "listen success\n";

1.3服务端获取连接

TCP服务器在与客户端进行网络通信之前,服务器需要先获取到客户端发送的连接请求。

accept函数:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

 参数解释:

返回值说明:其返回值也是一个文件描述符,前面第一次创建的listensock_是用于不断的去获取客户端发来的连接请求,收到连接请求后会再创建一个套接字(也就是其返回值),该套接字(也就是其返回值)用于为本次accept获取到的连接提供服务。

sockfd:特定的监听套接字,表示从该监听套接字中获取连接,进行通信。

addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。

addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。

 代码:

  void start()
    {
        threadpool<Task> *p = threadpool<Task>::getInstance();
         p->start();
        while (true)
        {
            struct sockaddr_in peer;   //用于获取对端信息
            socklen_t len = sizeof(peer);
            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (serviceSock < 0)
            {
                continue;             // 不断的去获取客户端发送来的请求
            }
            std::cout << "获取链接成功"
                      << " "
                      << "servericeSock=" << serviceSock << std::endl;

            // 获取客户端基本信息
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIp = inet_ntoa(peer.sin_addr);
           
          //  startserver(serviceSock, peerPort, peerIp); 下面说明

    }

 1.4服务端提供服务

当服务器与客户端建立关联后,客户端就可以向服务器发送数据了,之后服务器就应该接受这些数据,并作出对应的处理。其实二者建立关联后,该accept函数的返回值就是对应的文件描述符,服务器可以从中读取,发送数据。(上面代码最后一段调用下面函数)

要用到read函数,write函数,这里不在介绍了。

void startserver(int sock, uint16_t peerPort, std::string peerIp)
    {
        char inbuffer[1024];            //将读入的数据放入inbuffer中
        assert(sock > 0 && !peerIp.empty());

        while (true)
        {
            ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
            if (s > 0)
            {
                inbuffer[s] = '\0';
                std::cout << peerIp << " " << peerPort << " "
                          << "client>>" << inbuffer << std::endl;

                for (int i = 0; i < s; i++)  //将小写字母转为大写,再发送给客户端
                {
                    if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                        inbuffer[i] = toupper(inbuffer[i]);
                }

                write(sock, inbuffer, strlen(inbuffer));  //将数据发送给客户端
            }
            else if (s == 0)
            {
                std::cout << peerIp << " " << "clinet quit\n";
                break;
            }
            else
            {
                std::cerr << "读取错误\n";
                break;
            }
        }
        close(sock);
    }

 在上面代码中,提供服务的函数正常情况下是一个死循环,若是单进程的话,服务器也就一次只能为一个客户端提供服务,这显然是不合理的。服务器也该可以为许多客户端提供服务的。所以该提供服务的函数应该让创建的子进程或者创建的线程去执行。

创建子进程执行任务代码:

当子进程退出时会给父进程发送SIGCHLD信号,若父进程对该进行捕捉,并将该信号的处理动作设置为忽略,那么父进程可以继续执行自己的代码。

signal(SIGCHLD, SIG_IGN);//这只在LInux中有效
pid_t id = fork();                             
if (id == 0)
{
    close(listenSock_);
    startserver(serviceSock, peerPort, peerIp);//创建的子进程进行服务
    exit(0);
}
close(serviceSock);

也有另外一种写法:当父进程创建子进程后,再让该子进程fork,创建孙子进程,让孙子进程去执行任务代码,后子进程直接退出,父进程可以直接等待子进程。而该孙子进程变成孤儿进程,由操作系统管理,退出后,操作系统会对其进行回收处理。

pid_t id1 = fork();                          
if (id1 == 0)
{
    pid_t id2 = fork();
    if (id2 == 0)
    {
        startserver(serviceSock, peerPort, peerIp);//孙子进程
        exit(0);
    }
    exit(0);//子进程直接退出,孙子进程被bash管理
}

pid_t ret = waitpid(id1, nullptr, 0); //父进程可以直接阻塞式等待
 close(serviceSock);

二.客户端

与上文介绍的UDP客户端类似,同样不需要自己主动去绑定。

2.1创建套接字

class ClientTcp
{
public:
    ClientTcp(std::string ip, uint16_t port)
        : ip_(ip), port_(port), sock_(-1)
    {
    }
    void init()
    {
        sock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sock_ < 0)
        {
            std::cerr << "创建套接字失败\n";
            exit(-1);
        }
        std::cout << "创建套接字成功" << " " << "sock_=" << sock_ << std::endl;
    }

private:
    uint16_t port_;
    int sock_;
    std::string ip_;
};

 2.2客户端获取连接

TCP服务器在与客户端进行网络通信之前,客户端需要向服务器发送连接请求。

connect函数:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数解释:

sockfd:特定的套接字,表示通过该套接字发起连接请求。
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入的addr结构体的长度。

返回值说明:成功返回0.失败返回-1。
 

void init()
{
    sock_ = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_ < 0)
    {
        std::cerr << "创建套接字失败\n";
        exit(-1);
    }
    std::cout << "创建套接字成功"
        << " "
        << "sock_=" << sock_ << std::endl;

    struct sockaddr_in server;        //填充服务端的信息
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(port_);
    inet_aton(ip_.c_str(), &server.sin_addr);

// 向服务端发起连接请求
    if (connect(sock_, (const struct sockaddr*)&server, sizeof(server)) != 0)
    {
        std::cerr << "connect: " << strerror(errno) << std::endl;
        exit(-1);
    }
    std::cout << "连接成功\n";
}

 2.3客户端发送需求

当客户端发送连接请求,当服务端收到请求后,连接成功后,二者就可以进行通信了。这里模拟客户端向服务器发送数据,后再接受服务器发来的数据。同样是用到read与write函数。

代码:

 void start()
{
    std::string outbuffer;
    std::string inbuffer;
    while (true)
    {
        std::cout << "请输入信息>>";
        std::getline(std::cin, outbuffer);

        ssize_t s = write(sock_, outbuffer.c_str(), outbuffer.size());
        if (s > 0)
        {
            inbuffer.resize(1024, 0);
            ssize_t s = read(sock_, (char*)inbuffer.c_str(), 1024);
            if (s > 0)
            {
                inbuffer[s] = '\0';
                std::cout << "server>> " << inbuffer << std::endl;
            }
            else
                break;
        }
    }
    close(sock_);
}

三.填充命令行参数

3.1客户端

int main(int argc, char *argv[])
{

    if (argc != 2 && argc != 3)
    {
        std::cerr << "Usage:\n\t" << argv[0] << " port ip" << std::endl;
        std::cerr << "example:\n\t" << argv[0] << " 8080 127.0.0.1 \n"
                  << std::endl;
        exit(-3);
    }
    std::string ip;
    uint16_t port = atoi(argv[1]);
    if (argc == 3)
        ip = argv[2];
    ServerTcp *T = new ServerTcp(port, ip);
    T->init();
    T->start();
    return 0;
}

3.2服务端

int main(int argc, char *argv[])
{

    if (argc != 3)
    {
        std::cerr << "Usage:\n\t" << argv[0] << " port ip" << std::endl;
        std::cerr << "example:\n\t" << argv[0] << " 127.0.0.1 8080  \n"
                  << std::endl;
        exit(-3);
    }
    std::string ip;
    uint16_t port = atoi(argv[2]);
    ip = argv[1];
    ClientTcp *C = new ClientTcp(ip, port);
    C->init();
    C->start();
    return 0;
}

3.3结果测试

 四.线程版本服务端

4.1线程版

同样是对该执行任务的函数进行处理。在创建线程对应执行任务的函数时,该函数需要有对应客户端的IP,端口号。所以可以再写一个类来保存这些信息,后用一个指向该类的指针当参数,传给该执行任务的函数即可。

对应保存客户端数据的类:

class pthreadStart
{
public:
     pthreadStart(ServerTcp* thi,uint16_t clientPort,std::string clientIP,int sock)
     :this_(thi)
     ,clientPort_(clientPort)
     ,sock_(sock)
     ,clinetIp_(clientIP)
     {

     }
    uint16_t clientPort_;//对应客户
    std::string clinetIp_;
    int sock_;
    ServerTcp* this_;    //通过该指针调用处理任务函数
};

 执行任务函数:该函数在类内,用static修饰,去掉this指针

 static void* Routine(void* args)
{
    pthread_detach(pthread_self());
    pthreadStart* p = static_cast<pthreadStart*>(args);
    p->this_->startserver(p->sock_, p->clientPort_, p->clinetIp_);
}
void start()
{
    while (true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int serviceSock = accept(listenSock_, (struct sockaddr*)&peer, &len);
        if (serviceSock < 0)
        {
            continue;
        }
        std::cout << "获取链接成功"
            << " "
            << "servericeSock=" << serviceSock << std::endl;

        // 获取客户端基本信息
        uint16_t peerPort = ntohs(peer.sin_port);
        std::string peerIp = inet_ntoa(peer.sin_addr);

        pthread_t tid;                                
        pthreadStart* p = new pthreadStart(this, peerPort, peerIp, serviceSock);
        pthread_create(&tid, nullptr, Routine, (void*)p);

    }
}

4.2线程池版

封装一个线程的类,把创建线程的函数放入该类内,并提供相应接口。

线程池介绍:Linux中线程池的制作_"派派"的博客-CSDN博客

void start()
{
    threadpool<Task>* p = threadpool<Task>::getInstance();//创建一个线程池
    p->start();                                           //线程创建,在等待任务到来
//***************************************************************************
    while (true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int serviceSock = accept(listenSock_, (struct sockaddr*)&peer, &len);
        if (serviceSock < 0)
        {
            continue;
        }
        std::cout << "获取链接成功"
            << " "
            << "servericeSock=" << serviceSock << std::endl;

        // 获取客户端基本信息
        uint16_t peerPort = ntohs(peer.sin_port);
        std::string peerIp = inet_ntoa(peer.sin_addr);
       
       Task t(peerPort,peerIp,serviceSock); //把对应客户端信息放入任务
       p->push(t);                          //线程池加入任务
    }

任务代码:暂且把处理任务函数放入任务代码中,每个任务还保存有对应客户端的IP,端口号,以及对应套接字。

class Task
{
public:
    Task(uint16_t clientPort, std::string clientIP, int sock)
      : sock_(sock), clinetIp_(clientIP), clientPort_(clientPort)
     {
     }

    void startserver(int sock, uint16_t peerPort, std::string peerIp)
    {
        char inbuffer[1024];
        assert(sock > 0 && !peerIp.empty());

        while (true)
        {
            ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
            if (s > 0)
            {
                inbuffer[s] = '\0';
                std::cout << peerIp << " " << peerPort << " "
                          << "client>>" << inbuffer << std::endl;

                for (int i = 0; i < s; i++)
                {
                    if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                        inbuffer[i] = toupper(inbuffer[i]);
                }

                write(sock, inbuffer, strlen(inbuffer));
            }
            else if (s == 0)
            {
                std::cout << peerIp << " "
                          << "clinet quit\n";
                break;
            }
            else
            {
                std::cerr << "读取错误\n";
                break;
            }
        }
        close(sock);
    }
    void run()
    {
     
      std::cout<<"线程:"<<pthread_self()<<"开始处理工作\n";
      startserver(sock_,clientPort_,clinetIp_);
      std::cout<<"线程:"<<pthread_self()<<"结束工作\n";
    }

public:
    uint16_t clientPort_;
    std::string clinetIp_;
    int sock_;
};

线程池代码:当创建的线程去拿到任务后,就去调用任务的处理函数,去处理任务。

template <class T>
class threadpool
{
public:
    threadpool(const threadpool<T> &) = delete;
    void operator=(const threadpool<T> &) = delete;
    static threadpool<T> *getInstance()
    {
        if (nullptr == instance) // 过滤重复的判断
        {
            if (nullptr == instance)
            {
                instance = new threadpool<T>;
            }
        }
        return instance;
    }
    threadpool(int nums = pthnums)
    {
        pthread_num = nums;
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
        is_start = true;
    }

    ~threadpool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    static void *Routine(void *argv) // 类内函数有this指针,将其设置为静态,argv接受this
    {
        pthread_detach(pthread_self());
        threadpool<T> *tp = static_cast<threadpool<T> *>(argv);
        while (true)
        {
            cout << "pthread[" << pthread_self() << "]running" << endl;
            tp->lockQueue();
            while (tp->isempty())
            {
                tp->waitTask();
            }
            T t = tp->pop(); // 线程拿到任务
            tp->unlockQueue();
            t.run();         //线程调用执行任务函数
        }
    }
    void start() // 创建pthread_num个线程
    {
        assert(is_start);
        for (int i = 0; i < pthread_num; i++)
        {
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, this);
        }
        is_start = false;
    }
    void push(const T x)
    {
        lockQueue();
        _q.push(x);
        unlockQueue();
        SignalTask();
    }

    T pop()
    {
        T x = _q.front();
        _q.pop();
        return x;
    }

private:
    // 封装的接口
    void lockQueue()
    {
        pthread_mutex_lock(&mutex_);
    }
    void unlockQueue()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void waitTask()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    void SignalTask()
    {
        pthread_cond_signal(&cond_);
    }
    bool isempty()
    {
        return _q.empty();
    }

private:
    queue<T> _q;
    pthread_mutex_t mutex_; // 互斥锁
    pthread_cond_t cond_;   // 信号量
    int pthread_num;        // 创建线程数量
    bool is_start;
    static threadpool<T> *instance;
};
template <class T>
threadpool<T> *threadpool<T>::instance = nullptr;

结果:

 改进:其实该处理任务的函数可以放在任务函数中,也可以放入线程池中,但为了代码的解耦性,可其放入server端的代码上,后面执行该函数时,可以通过回调的方法。但最好的方法是将处理任务的代码封装成一个类(仿函数),里面包含具体的处理方法,然后将该类的对象放入任务端(Task)。下面就介绍第一种方法。

例如:

server端代码,将任务处理函数的实现放在服务端,后面传给任务端。

 Task t(peerPort,peerIp,serviceSock,startserver);
 p->push(t);

任务端代码:

class Task
{
public:
     //typedef std::function<void (int, uint16_t,std::string)> callback_t;//类型的声明
     using callback_t = std::function<void(int, std::string, uint16_t)>;

     Task(uint16_t clientPort, std::string clientIP, int sock, callback_t func)
       : sock_(sock), clinetIp_(clientIP), clientPort_(clientPort), func_(func)
     {
     }

    void run()
    {
     
       std::cout<<"线程:"<<pthread_self()<<"开始处理工作\n";
       func_(sock_,clientPort_,clinetIp_);   //进行回调
      std::cout<<"线程:"<<pthread_self()<<"结束工作\n";

    }

public:
    uint16_t clientPort_;
    std::string clinetIp_;
    int sock_;

     callback_t func_; // 回调方法
                       // void(*p_)(int,uint16_t,std::string);
};

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值