Linux网络套接字之TCP网络程序

该博客围绕Linux服务器网络编程展开,介绍了socket、listen、accept等接口,给出了tcp_server、tcp_client等基础代码及运行结果。还阐述了多进程、多线程、线程池版本的实现方式,最后提及小写转大写的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 (。・∀・)ノ゙嗨!你好这里是ky233的主页:这里是ky233的主页,欢迎光临~icon-default.png?t=N7T8https://blog.csdn.net/ky233?type=blog

点个关注不迷路⌯'▾'⌯

目录

一、接口介绍

1.socket

2.listen

3.accept

3.connect

4.send

二.基础代码

1.tcp_server

2.tcp_server.hpp

 3.tcp_client

4.结果

三、多进程版本

四、多线程版本

五、线程池版本

六、变成小写转换大写的方法


一、接口介绍

1.socket

不做过多介绍,前文有

SOCK_STREM,TCP用这个

2.listen

#include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int listen(int sockfd, int backlog);

将TCP套接字设置成监听状态

  • 参数一:创建好的套接字
  • 参数二:目前无法理解,

3.accept

#include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

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

获取链接

  • 参数一:套接字
  • 参数二:输出型参数
  • 参数三:输出输入型参数
  • 返回值:成功返回成功的套接字,失败则返回-1

3.connect

#include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

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

根据创建好的套接字,让客户端具有连接服务器的能力,会自动绑客户端的ip和端口

  • 参数一:套接字
  • 参数二:输出型参数
  • 参数三:输入型参数

4.send

#include <sys/types.h>
       #include <sys/socket.h>

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);

基于TCP向目标返回消息

参数四:一般为0

5.recv

#include <sys/types.h>
       #include <sys/socket.h>

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

基于TCP来读消息

参数四:一般为0

二.基础代码

1.tcp_server

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


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


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;
}

2.tcp_server.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"

static void service(int sock, const std::string &client_ip, const uint16_t &client_port)
{
    // echo server
    char buffer[1024];
    while (1)
    {
        // 先读取
        // read和write可以直接被使用,套接字也就是文件描述符
        ssize_t s = read(sock, &buffer, sizeof buffer);
        if (s > 0)
        {
            buffer[s] = 0; // 直接将发送过来的数据当作字符串打印
            std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s = 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", client_ip.c_str(), client_port);
            break;
        }
        else // 读取失败
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, &buffer, sizeof buffer);
    }
}
class TcpServer
{

    const static int g_backlog = 20; // 不能太大也不能太小
public:
    TcpServer(uint16_t port, std::string ip = "")
        : port_(port), ip_(ip), listensock_(-1)
    {
    }

    void initServer()
    {
        // 1.创建套接字
        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_);

        // 2.bind,将服务器和端口信息进行绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str());
        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)))
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 因为TCP是面向连接的,所以正是通信的时候需要先建立连接
        // 3.开始监听
        if (listen(listensock_, g_backlog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    void start()
    {
        signal(SIGCHLD,SIG_IGN);//子进程退出会像父进程发送SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        while (1)
        {
            // 4.获取连接,服务器要获取客户端的连接,这样来方便返回
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // listensock_和listensock_有什么区别呢?
            // 其中listensock_通过这个套接字只是为了把底层的连接获取上来
            // 而listensock_才是真正的进行连接的
            int listensock = accept(listensock_, (struct sockaddr *)&src, &len);
            if (listensock < 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, listensock_: %d | %s : %d |\n",
                       listensock, client_ip.c_str(), client_port);
            // 开始进行通信
            // 单进程循环版,只能够一次处理一个客户端,处理完成了才能处理下一个,很明显是不可以的
            //这是因为单进程的,下面函数是个死循环,所以不退出就没办法获取新的连接
            //service(listensock_, client_ip, client_port);
            //多进程版本
            //创建子进程,那么子进程可以打开父进程曾经打开的fd吗?
            //是可以的,父进程创建子进程时候,子进程的文件描述符表,会继承自于父进程,所以会看到同一个文件
            pid_t id= fork();
            assert(id!=-1);
            if(id==0)//子进程
            {
                //子进程会不会继承父进程打开的文件与文件描述符fd呢?这是肯定的
                //子进程是来提供服务的,需不需要监听socket呢?
                service(listensock_, client_ip, client_port);

                close(listensock);//这里让父子关闭不需要的文件描述符,不然文件描述符会越来越少
                exit(0);//子进程退出会造成僵尸进程
            }
            close(listensock);//这里让父子关闭不需要的文件描述符,不然文件描述符会越来越少
            //父进程
            //waitpid是阻塞式等待,也会占用资源无意义,所以这里用signal

            
            
        }
    }

    ~TcpServer()
    {
    }

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

 3.tcp_client

#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;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    // 获取ip和port
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    // 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // client是不需要显示bind的,因为如果bind了就是一个非常具体的端口号了,多个客户端可能会出现冲突,可能会导致启动失败,所以不需要显示的bind
    // 让OS自动选择
    // 但是客户端必须拥有连接的能力
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = ntohs(port);
    server.sin_addr.s_addr = inet_addr(ip.c_str());

    if (connect(sock, (struct sockaddr *)&server, sizeof(server)))
    {
        std::cerr << "connect error" << std::endl;
        exit(3); // TODO
    }
    std::cout << "connect success" << std::endl;

    //连接成功直接通信
    while(1)
    {
        std::string line;
        std::cout<< "请输入# ";
        std::getline(std::cin,line);

        //发送数据
        send(sock,line.c_str(),line.size(),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)
        {
            break;
        }
        else
        {
            break;
        }
    }

    return 0;
}

4.结果

可以看到我们的server是由两个的,一个是子进程一个是父进程

三、多进程版本

上面的其实已经是多进程版本的了,但是我们使用的signal信号来实现子进程退出的,下面我们用另一种方式来实现

为什么我们创建了子进程之后还要再创建一下呢?这是因为我们让子进程退出之间再次fork一下,会留下一个孙子进程,孙子进程变成了孤儿进程,会被OS领养,会被OS自己退出,所以父进程等待是不会阻塞的!

tcp_server.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include "log.hpp"

static void service(int sock, const std::string &client_ip, const uint16_t &client_port)
{
    // echo server
    char buffer[1024];
    while (1)
    {
        // 先读取
        // read和write可以直接被使用,套接字也就是文件描述符
        ssize_t s = read(sock, &buffer, sizeof buffer);
        if (s > 0)
        {
            buffer[s] = 0; // 直接将发送过来的数据当作字符串打印
            std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s = 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", client_ip.c_str(), client_port);
            break;
        }
        else // 读取失败
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, &buffer, sizeof buffer);
    }
}
class TcpServer
{

    const static int g_backlog = 20; // 不能太大也不能太小
public:
    TcpServer(uint16_t port, std::string ip = "")
        : port_(port), ip_(ip), listensock_(-1)
    {
    }

    void initServer()
    {
        // 1.创建套接字
        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_);

        // 2.bind,将服务器和端口信息进行绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str());
        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)))
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 因为TCP是面向连接的,所以正是通信的时候需要先建立连接
        // 3.开始监听
        if (listen(listensock_, g_backlog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    void start()
    {
        // signal(SIGCHLD,SIG_IGN);//子进程退出会像父进程发送SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        while (1)
        {
            // 4.获取连接,服务器要获取客户端的连接,这样来方便返回
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // listensock_和servicesock有什么区别呢?
            // 其中listensock_通过这个套接字只是为了把底层的连接获取上来
            // 而servicesock才是真正的进行连接的
            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);
            // 开始进行通信
            // 单进程循环版,只能够一次处理一个客户端,处理完成了才能处理下一个,很明显是不可以的
            // 这是因为单进程的,下面函数是个死循环,所以不退出就没办法获取新的连接
            // service(listensock_, client_ip, client_port);

            // 多进程版本
            // 创建子进程,那么子进程可以打开父进程曾经打开的fd吗?
            // 是可以的,父进程创建子进程时候,子进程的文件描述符表,会继承自于父进程,所以会看到同一个文件
            //  pid_t id= fork();
            //  assert(id!=-1);
            //  if(id==0)//子进程
            //  {
            //      //子进程会不会继承父进程打开的文件与文件描述符fd呢?这是肯定的
            //      //子进程是来提供服务的,需不需要监听socket呢?
            //      service(listensock_, client_ip, client_port);

            //     close(listensock);//这里让父子关闭不需要的文件描述符,不然文件描述符会越来越少
            //     exit(0);//子进程退出会造成僵尸进程
            // }
            // close(listensock);//这里让父子关闭不需要的文件描述符,不然文件描述符会越来越少
            // //父进程
            // //waitpid是阻塞式等待,也会占用资源无意义,所以这里用signal

            // 2.2多进程版本,不用signal
            pid_t id = fork();
            if (id == 0)
            {
                if (fork() > 0)//这是因为我们让子进程退出之间再次fork一下,会留下一个孙子进程,孙子进程变成了孤儿进程
                //会被OS领养,会被OS自己退出,所以父进程等待是不会阻塞的!
                {
                    close(listensock_);
                    exit(0);
                    service(servicesock, client_ip, client_port);
                    exit(0);
                }
            }
            // 父进程
            waitpid(id, nullptr, 0);
            close(servicesock);
        }
    }

    ~TcpServer()
    {
    }

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

四、多线程版本

tcp_server.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include "log.hpp"

static void service(int sock, const std::string &client_ip, const uint16_t &client_port)
{
    // echo server
    char buffer[1024];
    while (1)
    {
        // 先读取
        // read和write可以直接被使用,套接字也就是文件描述符
        ssize_t s = read(sock, &buffer, sizeof buffer);
        if (s > 0)
        {
            buffer[s] = 0; // 直接将发送过来的数据当作字符串打印
            std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s = 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", client_ip.c_str(), client_port);
            break;
        }
        else // 读取失败
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, &buffer, sizeof buffer);
    }
}

class ThreadData
{
    public:
    int sock_;
    std::string ip_;
    uint16_t port_;
};

class TcpServer
{

    const static int g_backlog = 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 = "")
        : port_(port), ip_(ip), listensock_(-1)
    {
    }

    void initServer()
    {
        // 1.创建套接字
        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_);

        // 2.bind,将服务器和端口信息进行绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str());
        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)))
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 因为TCP是面向连接的,所以正是通信的时候需要先建立连接
        // 3.开始监听
        if (listen(listensock_, g_backlog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    void start()
    {
        // signal(SIGCHLD,SIG_IGN);//子进程退出会像父进程发送SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        while (1)
        {
            // 4.获取连接,服务器要获取客户端的连接,这样来方便返回
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // listensock_和servicesock有什么区别呢?
            // 其中listensock_通过这个套接字只是为了把底层的连接获取上来
            // 而servicesock才是真正的进行连接的
            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);

            //3.0多线程版本
            ThreadData *td = new ThreadData();
            td->sock_ = servicesock;
            td->ip_= client_ip;
            td->port_ = client_port;
            pthread_t tid;
            //在多线程这里是不用关闭特定的文件描述符的,因为主线程与副线程是共享文件描述符的
            pthread_create(&tid,nullptr,threadRoutine,td);
            //close(servicesock)//走到最后主线程结束还是要关闭的


            // 1.0开始进行通信
            // 单进程循环版,只能够一次处理一个客户端,处理完成了才能处理下一个,很明显是不可以的
            // 这是因为单进程的,下面函数是个死循环,所以不退出就没办法获取新的连接
            // service(listensock_, client_ip, client_port);

            // 2.0多进程版本
            // 创建子进程,那么子进程可以打开父进程曾经打开的fd吗?
            // 是可以的,父进程创建子进程时候,子进程的文件描述符表,会继承自于父进程,所以会看到同一个文件
            //  pid_t id= fork();
            //  assert(id!=-1);
            //  if(id==0)//子进程
            //  {
            //      //子进程会不会继承父进程打开的文件与文件描述符fd呢?这是肯定的
            //      //子进程是来提供服务的,需不需要监听socket呢?
            //      service(listensock_, client_ip, client_port);

            //     close(listensock);//这里让父子关闭不需要的文件描述符,不然文件描述符会越来越少
            //     exit(0);//子进程退出会造成僵尸进程
            // }
            // close(listensock);//这里让父子关闭不需要的文件描述符,不然文件描述符会越来越少
            // //父进程
            // //waitpid是阻塞式等待,也会占用资源无意义,所以这里用signal

            // 2.1多进程版本,不用signal
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     if (fork() > 0)//这是因为我们让子进程退出之间再次fork一下,会留下一个孙子进程,孙子进程变成了孤儿进程
            //     //会被OS领养,会被OS自己退出,所以父进程等待是不会阻塞的!
            //     {
            //         close(listensock_);
            //         exit(0);
            //         service(servicesock, client_ip, client_port);
            //         exit(0);
            //     }
            // }
            // // 父进程
            // waitpid(id, nullptr, 0);
            // close(servicesock);
        }
    }

    ~TcpServer()
    {
    }

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

五、线程池版本

代码自取

六、变成小写转换大写的方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值