【Linux网络编程】--TCP_Socket

前面我们提到,TCP是要提供可靠的通信,那么其所要做的工作就要比UDP要多的多了,下面我们先直接进行代码编写。

首先不论是TCP还是UDP,对于我们的服务端,都不需要显示的bindIP,只需要一个端口号即可。

对于IP地址,是使用随机绑定的方式。

下面我们对服务端进行编写

EchoTcpServer.hpp

首先就是其服务端的基本结构和我们UDP的一样的,就是一个初始化还有一个启动接口。

下面我们完成对初始化接口的编写:

首先,和UDP一样,我们得先创建套接字socket,不过和UDP不一样的是,我们的TCP中的传输方式要选择字节流的方式。

可以看到上面从建立socket到bind网络信息,和UDP是基本一样的。

但是要补充的是,我们的TCP是要面向连接的,那么我们的TCP服务器就要其处于一种listen监听状态。

那么就要使用下面这个接口:

其作用是对一个socket进行监测,让这个socket进行等待新链接到来。

其第一个参数:就是我们前面所创建的socket。

然后其返回值:

成功,那么就返回0,失败就返回-1。

就好比饭店的老板,其是一直在店里等待顾客的。

其第二个参数:

就是设置我们链接队列的长度。一般我们设置为16或者32。

可以看到有个端口号为8080的tcp服务。

如下是对服务端的初始化代码:

复制代码
void InitServer()
    {
        // 创建soclet
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "creata socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::DEBUG) << "create socket success"<<_sockfd;
        // 填充网络信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY; // 任意IP地址bind

        // bind
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::DEBUG)<<"create bind success";
        // tcp是面向连接的,所以TCP服务器要处于一种listen,监听状态
        int m = listen(_sockfd, gbacklog);
        if (m < 0)
        {
            LOG(LogLevel::FATAL) << "lisent error";
            exit(LISENT_ERR);
        }
        LOG(LogLevel::DEBUG)<<"create lisent success";
    }

下面我们来完成启动的接口,对于TCP的启动,我们不能直接和前面UDP的一样,一上来就开始发送信息,而是要先建立连接,然后才可以进行通信。

那么我们建立连接,要使用下面这个接口:

首先其第一个参数,就是前面我们在初始化的时候创建的socket。

而后面两个参数,就是帮我们获取客户端的socket的IP和port信息。

然后其返回值:

如果其连接成功,那么其会给我们返回一个文件描述符,那么我们前面创建的sockfd和这个文件描述符有何区别?

前面我们所创建是socket就相当于一个中介,其是将我们给这个新连接的文件描述符进行链接的,后续进行网络通信,就和这个accept所返回的文件描述符进行通信,那么我们也称为IOsocket。

也就是服务socket。

未来一个服务socket,那么就代表一条连接。

那么我们建立好了链接,那么就要进行处理了。

在我们的TCP中,我们可以使用和文件读取一样的接口进行读取操作。

当我们在进行读的时候,当读取到的数据长度为0,那么并不是出错,而是说对方不想和我们进行通信了,那么此时我们就可以退出循环,那么就可以断开连接,那么断开连接就是将我们的文件描述符关闭即可。

那么如果读出错,那么也不需要马上终止进程,一样只需要先将其连接终止掉先。

代码如下:

复制代码
void serverIO(int soclfd, InetAddr &address)
    {
        LOG(LogLevel::DEBUG) << "client info is:" << address.ToString();
        while (true)
        {
            char inbuffer[1024] = {0};
            // 先读取数据
            ssize_t n=read(soclfd,inbuffer,sizeof(inbuffer)-1);
            if(n>0)
            {
                inbuffer[n]=0;
                LOG(LogLevel::INFO)<<address.ToString()<<"sya#"<<inbuffer;

                //写回去给客户端
                std::string echo_string ="server echo#";
                echo_string+=inbuffer;

                write(sockfd,echo_string.c_str(),echo_string.size());
            }
            else if(n==0)
            {
                LOG(LogLevel::INFO)<<"client quit,address:"<<address.ToString();
                break;
            }
            else
            {
                LOG(LogLevel::ERROR)<<"client read error ,address:"<<address.ToString();
                break;
            }
            
        }
    }

如上就是我们服务端的基本代码,下面我们完成对用户端进行编写。

EchoTcpClient.cc

在用户端,首先,我们也要进行socket的创建,创建好socket后,那么我们就要进行IP和port的绑定,但是有个问题,就是对于IP 和prot是否需要进行显示bind呢?

为了防止在服务器端,出现用户的端口号冲突的问题,所以不需要我们去手动bind,OS会对我们进行随机bind端口号。防止端口号冲突。

那么我们用户端创建好socket后,那么又该干嘛呢?

那么就要去和我们的服务端创建连接了。

那么在用户端要使用如下接口进行连接:

那么当我们进行连接的时候,其会为我们自动进行bind。

复制代码
//建立连接和bind
    InetAddr serveraddress(server_port,server_ip);
    int n = connect(sockfd,(struct sockaddr*)serveraddress.GetNetAddress(),serveraddress.Len());
    if(n<0)
    {
        std::cerr<<"connect to"<<serveraddress.ToString()<<"failed!";
        exit(3);
    }
    std::cerr<<"connect to"<<serveraddress.ToString()<<"success";

    //进行通信
    while(true)
    {
        std::string line;
        std::cout<<"please Enter#";
        std::getline(std::cin,line);

        write(sockfd,line.c_str(),line.size());

        char inbuffer[1024]={0};
        ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));
        if(n>0)
        {
            inbuffer[n]=0;
            std::cout<<inbuffer<<std::endl;
        }
        else if(n==0)
        {
            std::cout<<"read enf of flie"<<std::endl;
            break;
        }
        else{
            std::cerr<<"read error!"<<std::endl;
            break;
        }

那么还有一个问题,就是当我们进行连接,是一定就马上连接成功么?

实际上,我们平时进行打游戏也会有感觉,有时候会进行多次连接才会连接上,所以我们进行TCP连接的时候,是进行判断是否连接成功,然后进行循环连接的情况。然后如果连接多次的情况,那么我们就会进行终止进程的感觉。

EchoTcpServer.hpp改写

前面我们所写的服务端,其是单线程的,那么就是说,当我们有用户再使用服务器的时候,其他用户再去使用,那么此时就不能马上得到消息反馈,所以呢,我们下面要对其进行改写,改写成多进程的方式,对于创建连接后,对于消息的收发,我们创建一个子进程来完成。

但是我们的主进程也要对这个子进程来进行回收,那么如果我们进行wait等待的话,那么不也是要进行阻塞等待么,不还是单进程的效果,虽然我们可以使用waitpid来设置非阻塞等待,但是这个效果也不是很好。我们要的多进程的版本是,我们的主线程只去进行连接的工作,而我们的子进程就去完成通信工作,那么我们可以在我们的建立连接前,进行一个信号捕捉的工作,然后将子进程退出的时候返回的退出信号进行捕捉,进行一个忽略处理,那么子进程的退出我们主进程也进行处理了。而且也不需要进行wait等待子进程退出了。

那么我们还有一个方法,就是我们创建了子进程后,我们的子进程又立马去创建一个子进程,也就是创建孙子进程,然后由孙子进程去处理通信工作,我们的子进程立马退出,那么我们的主进程在创建好子进程后,立马就会等待到子进程的退出,那么就可以马上返回到开始进行等待连接的工作了。然后对于孙子进程其就变成孤儿进程,那么后续会被我们的系统进行回收处理。

然后还要补充的是,对于我们的主进程,我们再创建了子进程后,那么我们就不需要连接后返回的那个文件描述符了,所以为了资源的利用效率,我们要将其关闭,对于子进程也是如此,我们子进程不关心连接工作,只需要进行通信即可,所以我们要将对于的连接文件描述符关闭。

代码如下:

复制代码
 void Start()
    {
        while (true)
        {
            // signal(SIGCHLD, SIG_IGN); // 最佳实践
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            // 建立连接
            int sockfd = accept(_lisentsockfd, (struct sockaddr *)&clientaddr, &len);
            if (sockfd)
            {
                LOG(LogLevel:
:WARNING) << "accept errr!";
                continue;
            }
            LOG(LogLevel::DEBUG) << "accept success,sockfd:" << sockfd;
            // 处理链接
            //单进程版本
            // InetAddr clientaddress(clientaddr);
            // serviceIO(sockfd, clientaddress);

            //2、多进程版本
            
            pid_t id = fork();
            if(id < 0)
            {
                LOG(LogLevel::FATAL) << "fork errr!";
                exit(FORK_ERR);
            }
            else if(id == 0)
            {
                // 关闭掉自己不需要的sockfd
                close(_listensockfd);
                // 子进程
                if(fork() > 0) exit(0); // 子进程直接退出

                // 孙子进程 - 孤儿进程
                InetAddr clientaddress(clientaddr);
                serviceIO(sockfd, clientaddress);
                close(sockfd);
                exit(0); // 重要
            }
            else{
                // 父进程
                // wait(id); waitpid: WNOHANG
                // 关闭掉自己不需要的sockfd
                close(sockfd);
                pid_t rid = waitpid(id, nullptr, 0);
                (void)rid;
            }


            close(sockfd);
        }
    }

上面我们完成的是从单进程到多进程的改写,那么创建进程的开销相对于创建线程还是比较大的,所以我们还可以将其改成多线程的方式。那么这样效率就高很多了。

但是还有个疑问就是,当我们创建新线程,那么我们的主线程不也要对新创建的线程进行回收么?

所以我们可以将新线程设置成分离状态,那么我们主线程也就不用进行join了。

然后对于线程的处理函数,我们知道其参数只能是一个返回值为void*参数也为void*的函数,那么我们在类中可以将其设置成static静态全局的,但是又有个问题了,就是在这个函数中,我们又无法获取到this指针了,那么我们可以选择在这个服务端类中在创建一个类,这个类中我们设置一个服务端指针,然后在线程任务的时候将这个指针进行传入,那么我们就可以得到this指针了,那么在该函数中就可以获取到服务端方法了。

代码如下:

复制代码
class ThreadData
    {
    public:
        ThreadData(TcpServer *ts, int sockfd, const InetAddr &addr)
            : _this(ts), _sockfd(sockfd), _addr(addr)
        {
        }
        ~ThreadData()
        {
            close(_sockfd);
        }

    public:
        TcpServer *_this;
        int _sockfd;
        InetAddr _addr;
    };

    static void *thread_routine(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        pthread_detach(pthread_self());
        td->_this->serviceIO(td->_sockfd, td->_addr);
        delete td;
        return nullptr;
    }

 //ersion 2: 多线程版本
            pthread_t tid;
            InetAddr clientaddress(clientaddr);
            ThreadData *td = new ThreadData(this, sockfd, clientaddress);
            pthread_create(&tid, nullptr, thread_routine, (void *)td);

但是也要补充一点,就是未来我们服务端,其是一个长服务,那么如果我们如上一直创建多线程,那么我们的OS压力会很大,我们上面的方式,只合适在短服务上进行,对于长服务,我们后续会学习一个多路转发的方式。

所以不论是多线程还是多进程方式,我们上面的方式都只适合使用在多连接,短服务上,所以我们将IO服务进行一个修改,我们的线程,当执行了信息收发后,然后马上退出,当其要继续进行收发,然后其再重新进行申请。

对于这种短链接,短服务,可能我们创建线程的花销都要比处理任务的花销要大,所以我们可以引入我们前面写的线程池。

那么就这样就可以将我们的任务通过线程池进行调用完成。

而且当任务过多的时候,也会将其放入到任务队列中,在合适的时候再进行调用。

相关推荐
若汝棋茗1 小时前
UDP 调试难复现?先把目标端点、广播和消息记录管起来
网络·网络协议·udp
wuminyu1 小时前
JUC包的AQS与JVM的ObjectMonitor机制对比分析
java·linux·c语言·jvm·c++
闲云自留地1 小时前
课后作业-20260806-Linux iSCSI服务器
linux·运维·服务器
ACP广源盛139246256732 小时前
2026 PCIe互连芯片@ACP#国产替代格局解析:芯动科技领跑高端交换芯片赛道
大数据·网络·数据库·人工智能·分布式·嵌入式硬件
.冰块.2 小时前
P-SAN 实战:CentOS7 搭建 iSCSI Target 共享块存储
linux·服务器·iscsi
Kina_C2 小时前
Keepalived 单播模式实战:跨网络高可用的关键配置
linux·服务器·网络·keepalived
wWYy.2 小时前
阻塞和非阻塞
服务器·网络
zhang133830890752 小时前
CG-85D 水工大坝渗压监测振弦式传感器
运维·服务器·网络·人工智能·自动化
m0_743697592 小时前
磁盘管理的相关知识
linux