<网络>TCP代码协议编写

目录

一、网络通信其实质是什么?

二、编写代码 --------

三、编写一个Sock类

四、Tcp_Server类的编写

五、实现一个消息来回发送的服务器和客户端

六、附加的日志函数


一、网络通信其实质是什么?

这篇文章我们就主要实现一下如何使用TCP进行完整的网络通信

首先我们需要了解什么事TCP,TCP是一种传输控制协议,我们暂且先记住这个。

现在我们先开始,第一呢,我们要实现客户端和服务端的网络通信,两个不同的主机之间通信是通过什么?答案是每台主机的网络IP,在我们的电脑进行上网的时候,会自动给我们的电脑分配一个该局域网内的IP。 

好的,现在我们的两台主机相互的找到了对方,然后再看看我们要做的是什么,两台主机之间的通信对不对!再说清楚一点就是,我们需要使用一台主机上面的APP,给另一台主机上面的APP发消息,而APP再电脑上,启动起来叫做什么?我们对那些跑起来的应用程序用专业一点的称呼,叫做进程!!! ok,也就是需要大家明白我们实际上要实现的是! --  两台主机中的两个进程之间的通信!!!好,这段话了解到这里。过程类似下图:

<网络>TCP代码协议编写_第1张图片

 

接下来我们在来引入一个新的概念:端口号,大家先接受有这个东西,前面我们知道,现在我们通过IP两个主机可以互相找到了,现在的问题是:一台主机不可能永远只跑一个进程吧,就像我们同时启动QQ,微信,腾讯视频,浏览器等等,假设我们要两台主机之间的QQ通信,我们如何在那么多的进程中偏偏唯一的,坚定的选择它呢?,这个问题很关键。那么我们的设计者是如何解决这个问题的呢?接下来是重点:我们可以给每一个进程绑定一个端口号(如何绑定代码会体现的),并且规定一个端口号只能绑定一个进程,注意嗷:这里的一个进程是可以绑定多个端口号的。这个时候,我们在进行网络通信的时候,只需要带上对方的主机IP和进程绑定的端口号,就可以精准的进行网络通信了,是不是很简单呢,哈哈哈

二、编写代码 --------

那么代码上我们需要做那些事呢?

1.我们需要获取一个IP对吧

2.我们需要自定义一个端口号和这个IP绑定,形成唯一的对应关系,这里的关系,生成出来的就是socket套接字即:IP+port(端口号) 。

然后我们就可以实现通信的大致逻辑了,网络通信的底层是通过硬件网卡来进行的,对这些硬件的操作需要很高的权限,所以很显然,网络相关的接口是在系统层面的!!!

那么这里为了方便,我们先将封装一个Sock类,以用于服务器更好的编写。

Sock的功能有那些呢?这个类是关于TCP协议的,我们需要知道TCP通信的时候的细节:

TCP的Sock服务编写流程---

1.创建sock套接字:使用socket函数,获取一个用于网络的fd文件描述符,TCP中这个fd也就是sock套接字,叫做监听套接字

2.绑定IP+端口号:将这个fd描述符,通过bind函数,绑定IP+端口号,实现网络通信的必要条件

3.设置监听状态:将这个fd也就是sock套接字,通过listen函数设置为监听状态,这个sock也叫监听套接字

4.接受连接:再用accept函数,通过传入监听套接字,和一个struct sockaddr_in类型的输入输出型参数,接受来自客户端的连接请求,如果连接成功则返回一个套接字,两个进程的网络通信就通过这个新套接字完成!

解释一下第3,4点,因为TCP是面向连接的,所以当我们正式通信的时候,是需要先建立连接的,那么是不是,我们服务器就需要处于一种状态来等待我们来连接呢,这种状态就是监听状态,举个例子方便大家理解,就像我们去一些餐厅吃饭,白天去的时候,我们总是能吃到饭,为什么呢?因为那些餐厅的老板总是在等待客人的到来,以便能及时的为客人服务,这个监听状态就是如此,而被用于设置监听状态的套接字,就叫做监听套接字。

好的,现在大致流程清楚了,还有个问题是,我们如何来理解监听套接字和新返回的套接字呢?!

我们来讲个小故事来方便大家理解:假设嗷,你现在去一个地方旅游,去到了一个吃饭的地方,这个地方有很多的酒楼,而每个酒楼都比较内卷,都雇了一个人,这个人什么都不做,就做一件事,就是去马路边拉客人,路边有人就去拉客拉到店里,假设一个客人是你,这个拉客的叫做李四,李四拉你去自己的酒楼里去,你同意了,于是李四把你带到酒楼里去,让后扯着嗓门喊了一声,“后勤的服务器出来一个,来客人了,招呼着”,于是就出来了一个服务员张三,于是张三就来服务你,张三服务你的时候,这时李四又跑去路边拉客人了,循环往复。

上面的例子:

监听套接字---就是拉客李四,工作就是获取新链接

返回的新套接字---就是服务员张三

这里相信大家应该理解了,两个套接字的作用和意义了。

通过上面的过程,实现了两个连接,我们就可以开始通信啦!

可以直接使用read和write进行套接字的读写。

三、编写一个Sock类

下面我们就开始战斗!!!

首先写一个Sock类,其中包含了

1.创建sock套接字

2.绑定IP+端口号

3.设置监听状态

4.接受连接

以上的四个基本方法

//写一个Scok类
class Sock
{
public:
    Sock(){} // 无用
    int Socket(){} //1.创建套接字
    void Bind(int sock, std::string ip, uint16_t port){}  //2.实现套接字和IP、端口号的绑定
    void Listen(int listensock){} //3.设置监听套接字
    int Accept(){int listensock, std::string& ip, uint16_t& port}  //4.建立连接
    ~Sock(){} // 无用
};

 Socket()实现

通过上面的知识铺垫,我们需要调用socket系统函数获取套接字,如下

AF_INET:网络家族,使用的是IPV4

SOCK_STREAM:使用流式套接字,TCP是面向字节流的

0:默认就好

int Socket()
{
     int _listensock = socket(AF_INET, SOCK_STREAM, 0);
     if (_listensock < 0)
     {
        logMessage(FATAL, "creat listensocke [%d]-[%s]", errno, strerror(errno));
        exit(-1);
     }
     logMessage(NORMAL, "creat listensocke success is : %d",_listensock);
 
     return _listensock;
}

void Bind(int sock, uint16_t port, std::string ip)实现

bind系统函数需要用到sockaddr_in这个结构体,我们直接定义初始化传参就好

// 2.绑定IP+Port
void Bind(int sock, uint16_t port, std::string ip)
{
    sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());

    if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0)
    { 
        logMessage(FATAL, "bind listensocke [%d]-[%s]", errno, strerror(errno));
        exit(1);
    }
    logMessage(NORMAL, "bind listensocke success!");
}

void Listen(int listensock)实现

这个gblock,笔着现在的水平还不能详细解答,目前了解到的就是默认整型20就够了

// 3.设置监听状态
void Listen(int listensock)
{
    if (listen(listensock, gblock) < 0)
    {
        logMessage(FATAL, "set listensocke to listen status fail [%d]-[%s]", errno, strerror(errno));
        exit(1);
    }
    logMessage(NORMAL, "set listent status success");
}

int Accept(int listensock, std::string& ip, uint16_t& port)实现

其中的细节是,如果连接失败再进行下一次连接就行,就像是李四没有拉到客人,再去下一个客人就行,总不能就把店门关了吧

// 4.连接
int Accept(int listensock, std::string& ip, uint16_t& port)
{
    struct sockaddr_in addr;
    socklen_t len = sizeof addr;

    while (true)
    {
        int serverock = accept(listensock, (struct sockaddr*)&addr, &len);
        if (serverock < 0)
        {
            logMessage(ERROR, "get accept fail [%d]-[%s]", errno, strerror(errno));
        }
        else
        {
            logMessage(NORMAL, "accpet success [%d]-[%s]", errno, strerror(errno));
            ip = inet_ntoa(addr.sin_addr);
            port = ntohs(addr.sin_port);
            
                return serverock;
        }
    }
}

四、Tcp_Server类的编写

刚刚我们把Sock封装好了,现在编写这个类就好办多了。

我们直接就是一个大招,看看下面的代码是不是很简单了呢


class Tcp_server
{
public:
    Tcp_server(std::string ip = "", uint16_t port = 0)
    :_ip(ip), _port(port)
    {
        Sock c_server;
        // 1.创建监听套接字
        _listensock = c_server.Socket();
        // 2.绑定IP+Port
        c_server.Bind(_listensock, _port, _ip);
        // 3.设置监听状态
        c_server.Listen(_listensock);
    }

    void Start()
    {
        Sock c_server;
        // 4.连接
        while (true)
        {
            std::string ip;
            uint16_t port;

            int serversock = c_server.Accept(_listensock, ip, port);
            // 开始服务 
            server(serversock, ip, port); //服务函数等下再编写
            close(serversock);
        }
    }

    ~Tcp_server()
    {
        close(_listensock);
    }

private:
    std::string _ip;
    uint16_t _port;
    int _listensock;
};

附上server函数:这里实现的就是一个很简单的客户端发送给服务器,服务器再发送回去的逻辑,

大家注意到了没有,我们使用的是系统文件层面的write和read对套接字进行操作,这里我们就可以看出,其实套接字就是文件描述,是不是这里把网络和Linux中的一切皆文件!!!相呼应了呢!


void server(int sock, std::string ip, uint16_t port)
{
    char serverBuffer[1024];
    while (true)
    {
        ssize_t s = read(sock, serverBuffer, (sizeof serverBuffer) - 1);
        if (s < 0)
        {
            logMessage(ERROR, "read message fail [%d]-[%s]", errno, strerror(errno));
            break;
        }
        else if (s == 0)
        {
            logMessage(FATAL, "读端关闭");
            break;
        }
        else if (s > 0)
        {
            serverBuffer[s] = '\0';
            std::cout << "Client --- IP:" << ip << "Port:" << " " << port << std::endl;
            std::cout << serverBuffer << std::endl;
        }
        // 发送回去
        write(sock, serverBuffer, sizeof serverBuffer - 1);
    }
}

五、实现一个消息来回发送的服务器和客户端

最后我们来看看Tcp_server.cc和Tcp_client.cc实现简单的客户端和服务器

先是Tcp_server.cc的

#include 
#include 
#include "Tcp_server.hpp"

void Usage(std::string s)
{
    std::cout << "Usage "<< s <<  " port" << std::endl;
}

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

    std::string ip = "0.0.0.0";
    uint16_t port = atoi(argv[1]);
    Tcp_server server(ip, port);
    server.Start();

    return 0;
}

Tcp_client.cc的

注意,客户端是不用自己绑定IP和端口号的,它自己会自动分配,因为如果我们自己去绑定的话,可能会出现端口号冲突的问题,就会导致另外一个客户端起不来的情况。

#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"

void Usage(std::string s)
{
    std::cout << "Usage "<< s <<  "ServerIp ServerPort" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(-1);
    }
    // 编写客户端逻辑
    // 1.想要连接的IP
    // 2.想要连接的端口

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

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    // 客户端会自动分配端口号
    if (connect(sock, (struct sockaddr*)&addr, sizeof addr) < 0)
    {
        logMessage(FATAL, "client connect fail [%d]-[%s]", errno, strerror(errno));
        exit(-1);
    }

    logMessage(NORMAL, "connect success [%d]-[%s]", errno, strerror(errno));

    while (true)
    {
        std::string line;
        std::cout << "输入发送消息:";
        std::getline(std::cin, line);

        // write(sock, line.c_str(), sizeof line.c_str());
        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 << "recv server message:" << buffer << std::endl;
        }
    }

    return 0;
}

我们将它们编译后,运行起来看看:

这里采用的是Centos7.6版本的操作系统

先把服务器跑起来,这里注意,我们没有指明是服务器是主机的哪一个IP地址,因为我们在代码中使用的默认的“0.0.0.0”这个IP,表示服务器任意IP

 回车运行之后,我们再启动客户端

运行之后就是这样

 

 我们来看看

 我们发送一个消息试试

 可以看到,我们成功的发出去了消息,并且服务器也返回了消息,这里我们就实现了一个简简单单的TCP协议的网络通信服务器;

六、附加的日志函数

代码中的logMessage是自己编写的一个用于打印日志信息的函数,给大家分享出来吧

#pragma once

#include 
#include 
#include 
#include 
#include 

#define DEBUG   0
#define NORMAL   1
#define WARNING  2
#define ERROR    3
#define FATAL    4

#define DEBUG_SHOW

const char* gLevel[]  = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

void logMessage(int level, char* format, ... )
{
#ifndef DEBUG_SHOW
    if (level == DEBUG) return; 
#endif

    // 标准
    char stdBuffer[1024];
    const time_t curt = time(nullptr);
    struct tm* _curt = localtime(&curt);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s]-[%d年%d月%d日%d时%d分%d秒]", gLevel[level], 
            _curt->tm_year + 1900, _curt->tm_mon + 1, _curt->tm_mday, ((_curt->tm_hour + 12) % 24) + 12, _curt->tm_min, _curt->tm_sec);

    // 自定义
    char logBuffer[1024];
    va_list args;
    va_start(args, format);
    vsprintf(logBuffer, format, args);
    va_end(args);

    FILE* fd = fopen("log_tcp_server", "w");
    fprintf(fd, "%s-%s\n", stdBuffer, logBuffer);
    fclose(fd);
}

你可能感兴趣的:(计算机网络,网络,tcp/ip,服务器)