Linux网络:使用TCP实现网络通信(服务端)

文章目录

      • [1. TCP网络程序的服务端(初始化)](#1. TCP网络程序的服务端(初始化))
        • [1.1 封装一个TcpServer类](#1.1 封装一个TcpServer类)
        • [1.2 创建socket套接字](#1.2 创建socket套接字)
        • [1.3 绑定套接字](#1.3 绑定套接字)
        • [1.4 监听套接字](#1.4 监听套接字)
      • [2. TCP网络程序的服务端(连接和通信)](#2. TCP网络程序的服务端(连接和通信))
        • [2.1 Accept建立连接](#2.1 Accept建立连接)
        • [2.2 提取客户端信息](#2.2 提取客户端信息)
        • [2.3 实现通信服务](#2.3 实现通信服务)
  • 序:在上以章中,我们详细介绍了UDP网络编程的核心内容,包括服务端和客户端的实现。服务端和客户端重点讲解了如何使用recvfrom接收数据和sendto发送数据,以及如何处理客户端地址信息;此外,还探讨了IP与端口相关知识,如公网IP绑定限制、知名端口范围等。而本篇文章将讨论TCP网络编程,使用TCP来实现网络通信,看看TCP的服务端与客户端与UDP又有怎样的差距和变化。

  • 补充:

在上一章中,我们完成了使用UDP时限网络通信的程序,我们就能用此来建造一个类似于qq群聊的聊天室,由于在Linux中一切皆文件,/dev/pts/目录下存放的就是多个终端的文件,于是我们就可以做到将程序运行的结果在其他终端打印。

有了这个,我们就可以将所有客户端的信息都打印到同一个终端中。

1. TCP网络程序的服务端(初始化)

1.1 封装一个TcpServer类

要想启动服务端,服务端至少要一个构造,一个析构,一个初始化和一个运行的接口!!!

复制代码
const int defualtfd = -1;

class TcpServer
{
public:
    TcpServer()
    :_listensock(defualtfd)
    {}

    void InitServer()
    {}
    
    void Start()
    {}
    
    ~TcpServer()
    {}
private:
    int _listensock;
};
1.2 创建socket套接字

想要使用TCP套接字,就必须先获取一个套接字,要用到socket函数来获取套接字,与获取UDP套接字不同,在获取TCP套接字的时候,第二个参数要使用字节流的选项,而非用户数据报,在上一章中,我们就说过TCP是面向字节流的,所以第二个选项选择SOCK_STREAM,第三个参数依旧填0。

复制代码
class TcpServer
{
public:
	 TcpServer(const uint16_t &port,const std::string &ip=defualtip)
    :_listensockfd(defualtfd)
    ,_ip(ip)
    ,_port(port)
    {}
	
    void InitServer()
    {
	    _listensockfd = socket(AF_INET,SOCK_STREAM,0);
        if(_listensockfd < 0)
        {
            lg(Fatal,"create sockfd error,erron: %d,strerror: %s",errno,strerror(errno));
            exit(SOCKET_ERR);
        }
        lg(Info,"create sockfd success, sockfd: %d",_listensockfd);
    }
private:
    int _listensock;
    uint16_t _port;
    std::string _ip;
};
1.3 绑定套接字

其中要绑定IP地址,要将点分十进制的字符串转化为in_addr的函数:inet_aton

要包含的头文件:

复制代码
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

该函数的第一个参数是传入要转化的字符串。
第二个参数是传入一个uint32_t的四字节地址

复制代码
const std::string defualtip ="0.0.0.0";
const uint16_t defualtport =1234;

class TcpServer
{
public:
	 TcpServer(const uint16_t &port,const std::string &ip=defualtip)
    :_listensockfd(defualtfd)
    ,_ip(ip)
    ,_port(port)
    {}
	
    void InitServer()
    {
	    //创建套接字
	    //...
	    
	    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));
        //local.sin_addr.s_addr = INADDR_ANY;

        if(bind(_listensockfd,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
            lg(Fatal,"bind error,erron: %d,strerror: %s",errno,strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info,"bind success, sockfd: %d",_listensockfd);
    }
private:
    int _listensock;
    uint16_t _port;
    std::string _ip;
};
1.4 监听套接字

到了这一步,TCP与UDP的不同就显现出来了,我们知道UDP是无连接的,而TCP是有连接的,也就是说,tcp在通信前,要先建立连接,所以要将自己的套接字变成监听状态,Tcp是面向连接的,服务器一般是比较"被动的",服务器一直处于一种,一直等待连接到来的状态,这样,当有客户端想要来连接是,服务端就能够连接到。
使用listen函数将套接字变成监听状态:
该函数的第一个参数表示要变成监听状态的套接字
第二个参数表示底层全连接的长度
RETURN VALUE返回值:

成功就返回0,失败就返回-1,并将错误码设置

复制代码
class TcpServer
{
public:
    void InitServer()
    {
	    //创建,绑定套接字
	    //...
	    //Tcp是面向连接的,服务器一般是比较"被动的",服务器一直处于一种,一直等待连接到来的状态
        if(listen(_listensockfd,backlog) < 0)
        {
            lg(Fatal,"listen error,erron: %d,strerror: %s",errno,strerror(errno));
            exit(LISTEN_ERR);
        }
        lg(Info,"listen success, sockfd: %d",_listensockfd);
    }
private:
    int _listensock;
    uint16_t _port;
    std::string _ip;
};

2. TCP网络程序的服务端(连接和通信)

2.1 Accept建立连接

因为TCP是面向连接的,所以在实现通信之前要先把连接建立起来,然后再根据连接进行通信,所以我们就要用到建立连接的函数accept

第一个参数:当前服务器设置为监听状态的套接字
第二和第三个参数:输出型参数,用来获取客户端的IP地址和端口号等信息,就可以知道是谁发的,标识客户端的唯一性
RETURN VALUE返回值:
关键在于accept的返回值,成功则返回一个文件描述符,失败则返回-1,错误码被设置

其中的listensock只是为了将连接从底层给到accept,真正实行网络通信的是accept后的sockfd!!!就好比一群学生去饭店吃饭,那个给学生们招呼进来的门口的宣传的店员就是listensock,他只负责将顾客拉进店里,其他的不管,拉进店里后再又其他的服务员,也就是sockfd,给他们安排座位,上菜等!!!

2.2 提取客户端信息

之前我们说了,accept能将客户端的信息提取出来,现在我们要将网络序列转化为主机序列了,端口号要从网络序列变成主机序列,IP地址要从4字节整数变成字符串。
其中的难点在于将IP地址从4字节整数变成字符串,在UDP的通信过程中我们是用inet_ntoa函数来实现转化的,但是该函数本身的使用可能会有线程安全的问题,因为man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。但是如果我们调用多次这个函数,为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。

所以我们这次使用一个新的函数inet_ntop
该函数的第一个参数:对应的协议家族
第二个参数:对应的要转为字符串的四字节地址
第三个参数:用户自己传入一个缓冲区用来存放该字符串
第四个参数:传入的缓冲区的大小

复制代码
class TcpServer
{
public:
   void Start()
    {
        //singal(SIGCHLD,SIG_IGN);
        lg(Info,"TcpServer is running");
        while(true)
        {
            //1.获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            //没有接收到就是阻塞的
            int sockfd = accept(_listensockfd,(struct sockaddr*)&client,&len);
            
            if(sockfd < 0)
            {
                lg(Fatal,"create sockfd error,erron: %d,strerror: %s",errno,strerror(errno));
                continue; 
            }

            uint16_t clientport = ntohs(client.sin_port);
            //std::string clientip = inet_ntoa(client.sin_addr);
            char clientip[32]; 
            inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));
            //2.根据新连接来进行通信
            lg(Info,"get a new link...,sockfd: %d,client ip: %s,client port: %d\n",sockfd,clientip,clientport);
            Service(sockfd,clientip,clientport);
            close(sockfd);
        }
    }
private:
    int _listensock;
    uint16_t _port;
    std::string _ip;
};

问题一:我们在实现网络通信的过程中,我们将IP地址和端口号都进行的网络序列的转化,但是,我们没有对要发送的内容进行网络序列的转化,为什么IP地址和端口号需要,而发送的内容不需要?

在套接字里面,正常的通信内容,我们所使用的接口,他会帮我们将要发送的数据进行主机序列转网络序列,我们不需要担心,至于为什么我们要手动将IP地址和端口号进行主机序列转网络序列,是因为,IP地址和端口号比较特殊是要给操作系统的,是需要我们手动去转的,所以在实际操作中我们是不需要考虑数据的大小端问题的!!!

2.3 实现通信服务

由于TCP是面向字节流的,所以,读取数据和写入数据直接用read和write就行了

复制代码
class TcpServer
{
public:
    void Service(int sockfd,const std::string &clientip,const uint16_t &clientport)
    {
        while(true)
        {
            //因为tcp是面向字节流的,所以直接用read就可以直接接收到消息
            char buffer[4096];
            ssize_t n =read(sockfd,buffer,sizeof(buffer));
            if(n > 0)
            {
                buffer[n]=0;
                std::cout<<"client say@: "<<buffer<<std::endl;
                std::string echo_string = "tcpserver say@: ";
                echo_string += buffer;
                write(sockfd,echo_string.c_str(),echo_string.size());
            }else if(n == 0)
            {
                lg(Info,"[%s:%d] quit,server close sockfd: %d",clientip.c_str(),clientport,sockfd);
                break;
            }else
            {
                lg(Warning,"read error,sockfd: %d,client ip: %s,client port: %d",sockfd,clientip.c_str(),clientport);
                break;
            }
        }
    }
private:
    int _listensock;
    uint16_t _port;
    std::string _ip;
};

总结:

本文围绕TCP网络编程核心流程展开,先封装TcpServer类实现服务端初始化(创建socket、绑定地址、监听连接),再讲解accept建立连接、inet_ntop解析客户端信息,最后通过read/write完成面向字节流的通信,完整呈现TCP服务端从搭建到交互的全流程,兼顾代码实操与原理阐释。

相关推荐
熙客3 小时前
阿里云负载均衡SLB的使用
网络·阿里云·云原生·云计算·负载均衡
Dovis(誓平步青云)4 小时前
《探秘 Linux 进程控制:驾驭系统运行的核心之力》
linux·运维·服务器
Guheyunyi4 小时前
用气安全与能效优化平台
运维·网络·人工智能·安全·音视频
思成不止于此4 小时前
软考中级软件设计师备考指南(四):I/O 技术、安全与可靠性 —— 综合应用篇
网络·笔记·学习·信息安全·总线系统·i/o 技术·可靠性计算
羑悻的小杀马特4 小时前
从监听风险到绝对隐私:Zoom偷听门后,Briefing+CPolar重新定义远程会议安全标准
网络·安全·briefing
YouEmbedded5 小时前
解码Linux文件IO之系统IO
linux·服务器·文件io·系统io接口
半桔5 小时前
【网络编程】从数据链路层帧头到代理服务器:解析路由表、MTU/MSS、ARP、NAT 等网络核心技术
linux·运维·服务器·网络
ZLRRLZ6 小时前
【ProtoBuffer】protobuffer的安装与使用
服务器·网络
像素之间9 小时前
HTTP之content-disposition
网络·网络协议·http