网络编程套接字

目录

  • 理解源IP地址和目的IP地址
  • 认识端口号
  • 认识TCP协议
  • 认识UDP协议
  • 网络字节序
  • sockaddr结构
  • 简单的TCP网络程序
    • socket
    • bind
      • 服务端代码
    • 客户端
  • 简单TCP网络程序
    • listen
    • accept
    • connect
    • 服务端
    • 客户端
  • 守护进程
    • setsid

理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
两台主机互相通信,一定是一台主机把数据交给另一台主机,所以发送数据的主机,它发出去的报文里一定会携带自己的IP和对方的IP。就好比唐僧,走到哪儿都要告诉别人,都能告诉别人我从哪来到哪去。那么一个报文一定要携带原IP和目的IP的。原IP是为了方便别人把消息再回给你,那么目的IP是为了那么方便我们在路上根据IP地址进行路由,并且确认我们最终的报文已经到达对方主机。

认识端口号

网络通信的本质:其实是进程间通信

  1. 先将数据通过os,将数据发送到目标主机(手段) -----通过TCP/IP协议完成。IP可以表示互联网上唯一的一台主机
  2. 在在本主机将收到的数据,推送给自己上层的指定进程 ------用端口号标识自己主机上网络进程的唯一的一个进程

端口号(port)是传输层协议的内容.

  • 端口号是一个2字节16位的整数uint16_t
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用

一个进程可以绑定多个端口号(比如你有手机号,身份证号)但是一个端口号不可以被多个进程绑定(比如你的身份证号可以代表很多人吗)。

IP地址 + 端口号(Port) = 互联网中的唯一个进程(目前这样理解)

是通过IP+PORT构建进程唯-一性,来进行的基于网路的进程问通信!

(源IP,源端口) + (目标IP,目标端口号) = socket通信

端口号可以是进程PID吗,进程PID也不是唯一的吗?
如果用进程PID能实现这个功能的。但谁是更好的呢?如果你用了PID,那么这就有问题了。你用TD标识网络标识进程的通信绝对没问题,但是如果你用PID来做了,那么这里就有这样的问题。不是所有的进程都要进行网络通信。那么只有部分进程可能会网络通信。我们就很难区分清楚哪些进程是进行网络通信的,哪些不是。你要区分你就得加字段。你都加字段了,为什么不用端口呢。这是其一,当然,这不是最重要的。最重要的是。如果你用PID来作为,那么网络当中标识该进程。那么你换句话说呢,你在网络,就是网络功能的设计上,你必须得使用PID这样的字法。而PID是一个操作系统层面进程管理的概念。也就意味着你的网络模块儿也要包含进程管理的部分,要不然你无法认识端口。所以这样的话呢,就增加了系统当中进程管理和网络管理的耦合度。那么在设计字段的时候呢?你既得考虑PID,那么同学们,未来你改一下PID,你是不还可能影响网络?所以我们的设计者就认为没必要。我们设计单独端口号,你是你的,我是我的,我们不要燃。那么这样我们就能在逻辑上实现很好的解耦,这才是最重要的理由。

认识TCP协议

TCP(Transmission Control Protocol 传输控制协议)

  • 传输层协议
  • 有连接
  • 可靠传输(如果数据在传输的过程中,数据丢失的,会重新补发)
  • 面向字节流

认识UDP协议

UDP(User Datagram Protocol 用户数据报协议)

  • 传输层协议
  • 无连接
  • 不可靠传输(如果数据在传输的过程中,数据丢失的,那就真的丢失, 发送双方都不关心,对方是否收到)
  • 面向数据报

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

网络编程套接字_第1张图片

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

网络编程套接字_第2张图片

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

sockaddr结构

网络编程套接字_第3张图片

简单的TCP网络程序

socket

网络编程套接字_第4张图片

bind

网络编程套接字_第5张图片

云服务器,不需要bind ip地址,需要让服务器自己指定IP地址,自己本地装的虚拟机,或者物理机器是允许的bind ip地址,为什么?
云服务器上不是,只有一张网卡,它可能有多张网卡?也可能多张网卡当中配有多个IP。如果你的服务器配了两个IP,IP1和IP2,而你自己写的软件udp服务器绑定的时候只绑定了某一个IP,那也就意味着你的服务器上只能接收来自于某一个IP上面给你递交的数据报。所以呢我们就很很明显的就可能会导致你的服务器处理数据量就变少了。那么我们最想期望的就是,只要是发送到这台机器上的数据。那么我们到了就我的机器上,我们接下来要做的不应该用IP来甄别数据,而剩下的就是只要是给我特定端口的,你都转给我。

网络编程套接字_第6张图片

网络编程套接字_第7张图片

服务端代码

udp_server.hpp

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

enum
{
    SOCKET_ERR = 1,
    BIND_ERR
};

const static uint16_t default_port = 8888;
class UdpServer
{
public:
    UdpServer(uint16_t port = default_port) : port_(port)
    {
        std::cout << "server addr: " << port_ << std::endl;
    }
    void InitServer()
    {
        // 1. 创建socket接口,打开网络文件
        sock_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock_ < 0)
        {
            std::cerr << "create socket error: " << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        std::cout << "create socket success: " << sock_ << std::endl; // 3

        // 2. 给服务器指明IP地址(??)和Port
        struct sockaddr_in local; // 这个 local 在哪里定义呢?用户空间的特定函数的栈帧上,不在内核中!
        bzero(&local, sizeof(local));//local将数据置0
        

        local.sin_family = AF_INET; // AF_INET和PF_INET一样是宏替换
        // 网络传输包文中要携带[源ip,源端口 目标ip,目标端口]
        // 所以ip和端口号转化成为网络序列
        local.sin_port = htons(port_); // port_端口号是主机序列,要将主机序列端口号转换成网络序列端口号
        // 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 不能强制类型转换
        // 2. 需要将主机序列转化成为网络序列
        // local.sin_addr.s_addr = inet_addr(ip_.c_str());
        // inet_addr将点分十进制的字符串转化成in_addr_t类型,同时将主机序列ip地址转化成为网络序列
        // 3. 云服务器,或者一款服务器,一般不要指明某一个确定的IP
        local.sin_addr.s_addr = INADDR_ANY; // 让我们的udpserver在启动的时候,bind本主机上的任意IP
        // #define	INADDR_ANY		((in_addr_t) 0x00000000) 可以主机转网络但是没有意义因为是全零htonl(INADDR_ANY)

        // typedef uint32_t in_addr_t
        // 服务端bind将填充好套接字字段,文件字段,绑定关联成网络文件,因为这些数据是在栈上的使用bind才能绑定成网络的
        if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            std::cerr << "bind socket error: " << strerror(errno) << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "bind socket success: " << sock_ << std::endl; 
         
    }
    void Start()
    {
        char buffer[1024];
        while (true)//云服务器是要24小时运行的所以是死循环
        {
            // 收
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer); // 这里一定要写清楚,未来你传入的缓冲区大小
            int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
                buffer[n] = '\0';
            else
                continue;

            std::cout << "recv done ..." << std::endl;

            // 提取client信息 -- debug
            // 结构体peer是通过网络发送过了的,所以 需要网络序列将转化成为主机序列
            // 这里网络序列将转化成为主机序列的函数可以理解为,都是传值调用没有改变原值
            std::string clientip = inet_ntoa(peer.sin_addr); // inet_ntoa将转化成点分十进制的字符串的char* 类型,同时将网络序列ip地址转化成为主机序列
            uint16_t clientport = ntohs(peer.sin_port);      // 将网络序列转化成为主机序列
            //上面的操作只是读取并没有改变peer本身的内容
            std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;

            sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, sizeof(peer));
        }
    }
    ~UdpServer() {}

private:
    int sock_;
    uint16_t port_;
   
};

udp_server.cc

#include 
#include 
#include 
#include 
#include "udp_server.hpp"

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " port\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<UdpServer> usvr(new UdpServer);
    usvr->InitServer();
    usvr->Start();
    return 0;
}

客户端

#include 
#include 
#include 
#include 
#include 
#include 
#include 

// 127.0.0.1: 本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl; 
}

// ./udp_client serverip serverport
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]);

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(-1);
    }
    // client 这里要不要bind呢?要的!socket通信的本质[clientip:clientport, serverip:serverport]
    // 要不要自己bind呢?不需要自己bind,也不要自己bind,OS自动给我们进行bind -- 为什么?client的port要随机让OS分配防止client出现
    // 启动冲突 -- server 为什么要自己bind?1. server的端口不能随意改变,众所周知且不能随意改变的 2. 同一家公司的port号
    // 需要统一规范化

    // 明确server是谁
    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());

    while(true)
    {
        //多线程化??
        
        // 用户输入
        std::string message;
        std::cout << "[我的服务器]# ";
        // std::cin >> message;

        std::getline(std::cin,message);
        // 什么时候bind的?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP,1. bind 2. 构建发送的数据报文
        //发送
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        //接受
        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout <<  buffer << std::endl;
        }
    }

    return 0;
}

客户端什么时候bind的?
在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP,1. bind 2. 构建发送的数据报文发送

演示结果
网络编程套接字_第8张图片

每一台机器都有一个默认ip127.0.0.1本地环回,就表示当前主机,通常用来进行本地通信或着测试用。

问题 1
我在进行我们正常创建套接字,这里local.sin_port = htons(port_);,我在进行创建套接字绑定的时候,设置端口和IP,以端口回为例。需要主机转网络。可是我发现为什么我在进行,收消息的时候,发消息的时候,我们并没有做任何的主机转网络的转化。难道这里就不需要做转化吗?
答案:不需要,因为这种传送数据的,那么recvfromsendto,它会自动在底层帮我们做大小端的转化。以及TCP的读写方法。也照样是进行自动为我们做大小端的转化,只有你在绑定阶段啊,在启动服务器时的,那么有一些属性是需要由用户去把它大小的转化的

简单TCP网络程序

listen

网络编程套接字_第9张图片

  • listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大;
  • listen()成功返回0,失败返回-1;

accept

网络编程套接字_第10张图片

  • 三次握手完成后, 服务器调用accept()接受连接;
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给addr 参数传NULL,表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度
  • 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

connect

网络编程套接字_第11张图片

  • 客户端需要调用connect()连接服务器;
  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
  • connect()成功返回0,出错返回-1;

我们以前在讲udp的时候,我们发现这个套阶段udp通篇只有一个套接字呀,为什么accept的返回值还是一个套接字(文件描述符)?
我们accept它就要传入的这个参数,我们称为监听套接字,就好比我们进入一个饭店,门外有招揽客人的,饭店内有真正提供服务,监听套接字就是门外有招揽客人(只有一个)。而我们每一次一个链接到来时,我们的accept会返回一个文件描述符,那么返回值,它也的一个文件描述符,它就它的作用呢,是真正给用户提供数据IO服务的,

如果TCP没有多执行流的话,它只能处理一个客户端。那为什么UDP就可以处理多个客户端呢?
UDP它单进程能受理多客户端的原因是因为他收取数据是不需要链接。不需要accept等待,而今天我们在读取时,我们是在进行获取新连接需要accept等待。而当你的主执行流进程或线程在获取新链接时。你的主执行流进入service,来提供服务时,给一个人进行数据读取的候。那么在给这一个人进行数据读取的时候,那么我们对应的主执行流是不是就没有办法再去accept了?就导致我们的服务一次只能给一个客户端提供服务。

服务端

tcpServer.hpp

#pragma once

namespace tcp_server
{
    using func_t = std::function<std::string(const std::string)>;

    class tcpServer;
    class ThreadData
    {
    public:
        ThreadData(int fd, tcpServer* ts)
        :sock(fd)
        ,current(ts)
        {}
    public:
        int sock;
        tcpServer *current;
    };

    class tcpServer
    {
    public:
        tcpServer(func_t func, uint16_t port = 8080)
            :port_(port)
            ,func_(func)
        {}
        void initServer()
        {
            listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
            if(listenSock_ < 0)
            {
                std::cout<< "创建套接字失败"<< std::endl;
                exit(-1);
            }
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));//创建时建议初始化
            local.sin_family = AF_INET;
            local.sin_port = htons(port_);
            local.sin_addr.s_addr = htonl(INADDR_ANY);//可以不用转
            //local 数据是在栈上的使用bind才能绑定成网络的
            if(bind(listenSock_, (struct sockaddr*)&local, sizeof(local)) < 0)//与c++ std::bind不一样
            {
                std::cout<< "绑定失败"<< std::endl;
                exit(-1);
            }
            //监听
            if(listen(listenSock_, 32) > 0)
            {
                std::cout<< "监听失败"<< std::endl;
                exit(-1);
            }
        }

        void start()
        {
            while(true)
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                //获取连接
                int sock = accept(listenSock_, (struct sockaddr*)&client, &len);
                //sock 这个描述符才是真为提供服务的
                if(sock < 0)
                {
                    std::cerr << "accept error" << std::endl;
                    continue;
                }
                 // 提取client信息 -- debug
                std::string clientip = inet_ntoa(client.sin_addr);
                uint16_t clientport = ntohs(client.sin_port);
                //获取连接成功,开始进行处理
                // 5. 获取新连接成功, 开始进行业务处理
                std::cout << "获取新连接成功: " << sock << " from " << listenSock_ << ", "<< clientip << "-" << clientport << std::endl;
                pthread_t tid;
                ThreadData *td = new ThreadData(sock, this);
                pthread_create(&tid, nullptr, threadRoutine, td);
                // v4: 一旦用户来了,你才创建线程, 线程池吗??
                //使用线程池的时候,一定是有限的线程个数,一定是要处理短任务

            }
        }

         static void *threadRoutine(void *args)
        {
            pthread_detach(pthread_self());

            ThreadData *td = static_cast<ThreadData *>(args);
            td->current->service(td->sock);
            delete td;
            return nullptr;
        }

        void service(int sock)
        {
            char arr[1024] ={0};
            while(true)
            {
                //linux下一切皆文件
                ssize_t s =read(sock, arr, sizeof(arr) - 1);
                //read返回实际读取的字节数,读取失败返回-1
                if(s > 0)
                {
                    arr[s] = '\0';//文件中不存放\0
                    std::string res = func_(arr);
                    std::cout << ">>> " << res << std::endl;
                    write(sock, res.c_str(), res.size());

                } 
                else if(s == 0)
                {
                    //管道通信时,只有对方关闭文件是s才会等于0
                    close(sock);
                    std::cout <<" quit, me too" << std::endl;
                    break;
                }
                else
                {
                    close(sock);
                    break;
                }
            }
        }
        ~tcpServer()
        {}
    private:
        uint16_t port_;
        int listenSock_;
        func_t func_;
    };
}

tcpServer.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "error.hpp"
#include "tcpServer.hpp"
#include "daemon.hpp"

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}

std::string echo(const std::string& str)
{
    return str;
}

int main(int argc, char* argv[])
{
    // 准备工作
    if(argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port = std::atoi(argv[1]);
    std::unique_ptr<tcp_server::tcpServer> tcp(new tcp_server::tcpServer(echo, port));
    tcp->initServer();
    //将服务器守护进程化
    //Daemon();
    tcp->start();
    

    return 0;
}

客户端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include  
#include "error.hpp"

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    // 1. create socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        std::cerr << "socket error : " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }

    // client要不要bind? 要 
    // client要不要自己bind? 不要,因为client要让OS自动给用户进行bind
    // client不需要listen(监听)?
    //client不需要accept(获取连接)

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_aton(serverip.c_str(), &(server.sin_addr));

    int cnt = 5;
    while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
    {
        sleep(1);
        std::cout << "正在给你尝试重连,重连次数还有: " << cnt-- << std::endl;
        if(cnt <= 0) break;
    }
    if(cnt <= 0)
    {
        std::cerr << "连接失败..." << std::endl;
        exit(CONNECT_ERR);
    }

    char buffer[1024];
    // 3. 连接成功
    while(true)
    {
        std::string line;
        std::cout << "Enter>>> ";
        std::getline(std::cin, line);

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

        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo >>>" << buffer << std::endl;
        }
        else if(s == 0)
        {
            std::cerr << "server quit" << std::endl;
            break;
        }
        else 
        {
            std::cerr << "read error: " << strerror(errno) << std::endl;
            break;
        }
    }
    close(sock);
    return 0;
}

网络编程套接字_第12张图片

守护进程

网络编程套接字_第13张图片

因为当前你在启动一个进程的时候,是在当前bach的会话当中,你先写一个任务,这个是种新起的任务,只有一个进程,就是你自己,那么你自己就是自称进程组组长,组成一个任务,你就是组长,

守护进程一但开启就与终端没有关系了
在这里插入图片描述

setsid

网络编程套接字_第14张图片

#pragma once
// 1. setsid();
// 2. setsid(), 调用进程,不能是组长!我们怎么保证自己不是组长呢?
// 3. 守护进程a. 忽略异常信号 b. 0,1,2要做特殊处理 c. 进程的工作路径可能要更改 /

//守护进程的本质:是孤儿进程的一种!
void Daemon()
{
    // 1. 忽略信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    // 2. 让自己不要成为组长
    if (fork() > 0)
        exit(0);
    // 3. 新建会话,自己成为会话的话首进程
    pid_t ret = setsid();
    if ((int)ret == -1)
    {
        std::cout<<"新建会话失败"<<std::endl;
        
        exit(-1);
    }
    // 4. 可选:可以更改守护进程的工作路径
    // chdir("/")
    // 5. 处理后续的对于0,1,2的问题
    int fd = open("/dev/null", O_RDWR);//“/dev/null”信息黑洞,或者垃圾桶,这个文件向他里面写什么也写不进去,向他里面读什么也读不到
    //“/dev/null”这个文件是必须要存在的,任何linux系统都要有这个文件
    if (fd < 0)
    {
        std::cout<<"处理0,1,2问题失败"<<std::endl;
        exit(-1);
    }
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd);
}

你可能感兴趣的:(网络,服务器)