Linux——网络套接字3|Tcp客户端编写②

根据我们前面写的服务器,server端需要绑定,而client要不要bind呢?

不需要,因为客户端一旦和一个非常具体的端口号绑定,可能会导致端口号绑定多个客户端,因此可能会出现某个客户端无法启动。而服务器需要明确的端口号,因为服务器面对的是众多的客户端,服务器端口号一旦被改,所有客户端可能会无法连接服务器。即,服务器端口号一经采纳便不再改变。

虽然客户端不需要bind,但一定需要端口号,这里让OS自动选择进行端口号选择。

对于客户端最需要的是连接别人的能力。这里用的端口叫connect

connect

第一个参数套接字,后俩个是输入型参数和结构体长度,含义就是想连接谁,即后俩个是目标服务器套接字信息。

connect成功返回0,否则返回-1.connect成功,代表该文件描述符和对端服务器建立了连接,

send

send是一个基于Tcp来向目标服务器发送数据的接口,前三个参数和write一模一样,flag参数一般默认设为0.

当启动服务器和客户端之后,双方自动建立好了连接

此时我们可以看到俩个sevrer一个是父进程,一个是子进程

这里我们让服务器把收到的信息返回给了客户端

客户端退了,服务端也进行了关闭,注意这里服务端是关闭了子进程

多进程版服务器

服务端多进程部分代码

这部分代码是什么意思?waitpid难道不会阻塞吗?

我们先查看运行结果

我们看到有俩个进程的父进程是1

为何创建子进程后还要再创建?

到这里,再fork之后,子进程立即退出。子进程一旦退出,可留下了自己创建的子进程,暂且称这个进程为孙子进程,即真正给用户提供服务的是孙子进程,子进程退出之后,孙子进程就变成了孤儿进程,孤儿进程会被OS领养。即OS在孤儿进程退出的时候,由OS自动回收孤儿进程。

由于子进程退出了,waitpid不会阻塞,孤儿进程由OS领养,即服务器回收资源的任务交给了OS。

服务器多线程版本

多线程这道理不用关闭特定的文件描述符,如果关了,服务器可能会被关,或服务本身被关。

如果我们没有pthread_join,该线程在结束的时候会产生内存泄漏的问题。如果没有pthread_join我们需要进行线程分离,

分离了线程,让分离线程去处理,服务端便不再关心退出的相关信息。创建完线程直接就让线程去忙。

来个客户端,就多一个线程

每退出一个客户端,线程就减少一个

服务器线程池版本

不同的线程为不同的用户提供服务

一般服务器进行业务处理,从连上起,要一直保持这个连接,这个连接叫长连接。我们可以让服务器处理完信息后,客户端关闭连接。这样可以避免n个客户端长期占据线程的问题。

我们可以写一个简单的小写字母转大写的服务器或其它简单的功能。

地址转换函数

字符串转in_addr的函数:

点分十进制IP转四字节IP,

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void

*addrptr

关于inet_ntoa

net_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是

否需要调用者手动释放呢?

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.

那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?

在APUE中, 明确提出inet_ntoa不是线程安全的函数;

但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;

tcp_server.hpp

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"

// namespace TcpSock{} TcpSock::TcpServer
// static void service(int sock, const std::string &clientip, const uint16_t &clientport)
// {
//     //echo server
//     char buffer[1024];
//     while(true)
//     {
//         // read && write 可以直接被使用!
//         ssize_t s = read(sock, buffer, sizeof(buffer)-1);
//         if(s > 0)
//         {
//             buffer[s] = 0; //将发过来的数据当做字符串
//             std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
//         }
//         else if(s == 0) //对端关闭连接
//         {
//             logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
//             break;
//         }
//         else{ //
//             logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
//             break;
//         }

//         write(sock, buffer, strlen(buffer));
//     }
//     close(sock);
// }

static void service(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name)
{
    // echo server
    //  同时在线10人
    //  所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    //  后面有其他方案!
    char buffer[1024];
    while (true)
    {
        // read && write 可以直接被使用!
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if (s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        { //
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}

static void change(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    // echo server
    //  同时在线10人
    //  所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    //  后面有其他方案!
    char buffer[1024];
    // read && write 可以直接被使用!
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        std::string message;
        char *start = buffer;
        while(*start)
        {
            char c;
            if(islower(*start)) c = toupper(*start);
            else c = *start;
            message.push_back(c);
            start++;
        }

        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 对端关闭连接
    {
        logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
    }
    else
    { //
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }

    close(sock);
}
static void dictOnline(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    // echo server
    //  同时在线10人
    //  所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    //  后面有其他方案!
    char buffer[1024];
    static std::unordered_map<std::string, std::string> dict = {
        {"apple", "苹果"},
        {"bite", "比特"},
        {"banana", "香蕉"},
        {"hard", "好难啊"}
    };
    // read && write 可以直接被使用!
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        std::string message;
        auto iter = dict.find(buffer);
        if(iter == dict.end()) message = "我不知道...";
        else message = iter->second;
        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 对端关闭连接
    {
        logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
    }
    else
    { //
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }

    close(sock);
}

// class ThreadData
// {
// public:
//     int _sock;
//     std::string _ip;
//     uint16_t _port;
// };

class TcpServer
{
private:
    const static int gbacklog = 20; // 后面再说
    // static void *threadRoutine(void *args)
    // {
    //     pthread_detach(pthread_self());
    //     ThreadData *td = static_cast<ThreadData *>(args);
    //     service(td->_sock, td->_ip, td->_port);
    //     delete td;

    //     return nullptr;
    // }
public:
    TcpServer(uint16_t port, std::string ip = "0.0.0.0")
        : _listensock(-1), _port(port),
          _ip(ip), _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {
    }
    void initServer()
    {
        // 1. 创建socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", _listensock); // 3

        // 2. bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
        // inet_aton(_ip.c_str(), &local.sin_addr);
        // local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
        if (listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }
    void start()
    {
        // signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        _threadpool_ptr->run();
        while (true)
        {
            // sleep(1);
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // fd(李四,王五等提供服务的服务员) vs _sock(张三 --- 获取新连接)
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            }
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",
                       servicesock, client_ip.c_str(), client_port);
            // verison4 --- 线程池版本
            // Task t(servicesock, client_ip, client_port, service);
            // Task t(servicesock, client_ip, client_port, change);
            Task t(servicesock, client_ip, client_port, dictOnline);
            _threadpool_ptr->pushTask(t);

            // version 3 --- 多线程版本
            // ThreadData *td = new ThreadData();
            // td->_sock = servicesock;
            // td->_ip = client_ip;
            // td->_port = client_port;
            // pthread_t tid;
            // //在多线程这里用不用进程关闭特定的文件描述符呢??1 0
            // pthread_create(&tid, nullptr, threadRoutine, td);
            // close(servicesock);

            // pthread_join();
            // version2.1 -- 多进程版
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // 子进程
            //     close(_listensock);
            //     if(fork() > 0/*子进程本身*/) exit(0); //子进程本身立即退出
            //     // 孙子进程, 孤儿进程,OS领养, OS在孤儿进程退出的时候,由OS自动回收孤儿进程!
            //     service(servicesock, client_ip, client_port);
            //     exit(0);
            // }
            // // 父进程
            // waitpid(id, nullptr, 0); //不会阻塞!
            // close(servicesock);

            // 开始进行通信服务啦
            // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // 很显然,是不能够直接被使用的! -- 为什么? 单进程
            // service(servicesock, client_ip, client_port);

            // version 2.0 -- 多进程版 --- 创建子进程
            // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?1 0
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
            //     // 子进程, 子进程会不会继承父进程打开的文件与文件fd呢?1, 0
            //     // 子进程是来进行提供服务的,需不需要知道监听socket呢?
            //     close(_listensock);
            //     service(servicesock, client_ip, client_port);
            //     exit(0); // 僵尸状态
            // }
            // close(servicesock); // 如果父进程关闭servicesock,会不会影响子进程??下节课
            // 父进程
            // waitpid(); //
        }
    }
    ~TcpServer() {}

private:
    uint16_t _port;
    std::string _ip;
    int _listensock;
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

tcp_server.cc

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

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->start();
    return 0;
}

tcp_slient.cc

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    bool alive = false;
    int sock = 0;
    std::string line;
    while (true) // TODO
    {
        if (!alive)
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }
            // client 要不要bind呢?不需要显示的bind,但是一定是需要port
            // 需要让os自动进行port选择
            // 连接别人的能力!
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(serverport);
            server.sin_addr.s_addr = inet_addr(serverip.c_str());

            if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                std::cerr << "connect error" << std::endl;
                exit(3); // TODO
            }
            std::cout << "connect success" << std::endl;
            alive = true;
        }
        std::cout << "请输入# ";
        std::getline(std::cin, line);
        if (line == "quit")
            break;

        ssize_t s = send(sock, line.c_str(), line.size(), 0);
        if (s > 0)
        {
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << "server 回显# " << buffer << std::endl;
            }
            else if (s == 0)
            {
                alive = false;
                close(sock);
            }
        }
        else
        {
            alive = false;
            close(sock);
        }
    }

    return 0;
}

makefile

.PHONY:all
all:tcp_client tcp_server

tcp_client:tcp_client.cc
    g++ -o $@ $^ -std=c++11 #-lpthread
tcp_server:tcp_server.cc #ThreadPool/%.cc
    g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
    rm -f tcp_client tcp_server

部分加了注释的代码以及ThreadPool:点击跳转

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

头发没有代码多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值