计算机网络基础之网络套接字socket编程(初步认识UDP、TCP协议)

绪论​

"宿命论是那些缺乏意志力的弱者的借口。 ------罗曼.罗兰",本章是为应用层打基础,因为在写应用层时将直接通过文本和代码的形式来更加可视化的理解网络,本章主要写的是如何使用网络套接字和udp、tcp初步认识。

话不多说安全带系好,发车啦(建议电脑观看)。

1.理解套接字

套接字 = ip + 端口

我们所有的网络通信行为:本质就是进程间通信!!

对双方而言

  1. 先把数据能到达自己的主机(ip)
  2. 找到指定的进程(port:端口号)
  3. IP地址用来标识互联网中唯一的一台主机
  4. 端口号用来标识该指定的机器中进程的唯一性
  5. 所以{ip,port}(ip+port):互联网中唯一一台主机的进程(套接字,socket )
  6. 在互联网中通过套接字进行通信(也就类似于是网络中的进程通信)

2.认识端口号

端口是是一个16位的数字:uint16_t port,它用来标识当前主机上的唯一的一个网络进程,以及让网络进程 和 port进行绑定关联(让接收方确认是发给自己的某个进程)

2.1pid vs port

不难发现pid和port非常类似,但区分出来的原因是:

  1. 进程管理 和 网络管理 进行解耦(防止OS中进程改变影响网络通信)
  2. port用来专门进行网络通信
  3. 一个端口号只能和一个进程相关联(反之一个进程可以和多个端口号相关联)

socket:可以理解成打10086 + 转人工(工号):其中10086相当于ip、人工相当于port

3.TCP协议

  1. 有连接
  2. 可靠传输:tcp面对异常他会进行可靠性处理,丢包了重传,乱序了排成有序 ...(但也意味着复杂要做更多的工作)
  3. 面向字节流

4.UDP协议

  1. 无连接
  2. 不可靠传输:反之发数据后,不管数据是否异常(丢包,乱序...)(也意味着简单,适合于对数据可靠性要求不高的场景(直播))
  3. 面向数据报

tcp、udp:只有不同,没有好坏,分情况使用。


5.网络字节序

在网络中可能会有大端存储,小端存储

不懂的建议观看这篇blog

存储的方式可能因为不同的机器,存储方案也是不同的

其中在网络中:

  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  2. 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  3. 总结上述意思就是:组织规定了所有到达网络的数据,必须是大端,所有从网络收到数据的机器,都会知道数据是大端(这样也能方便的查看数据)

6.socket编程接口

6.1UDP协议

网络编程的时候,socket是有很多类型的

  1. unix socket:域间socket->同一台主机上的文件路径,命名管道,本主机内部进行通信
  2. 网络socket:主要使用ip+port进行网络通信(用的是传输层和网络层(tcp、udp))
  3. 原始socket:编写一些网络工具(应用层跳过传输层(不进行网络通信)直接到达网络层和数据链路层)

网络编程的时候,是由不同的场景的,理论上而言,我们应该给每种场景设计一套编程接口,而设计者只想用一套接口(改变传递参数就能实现找到对应的结构,基础的结构是struct sockaddr)
struct socket是一个通用的地址类型:

通过判断前两个字节(16位地址类型)来判断是哪一种类型

  1. 当第一个参数是AF_INET表示:网络通信
  2. 当第一个参数是AF_UNIX:域间套接

上述用的都是同一套API:


常用的socket套接字函数(能写UDP协议了)

  1. int socket(int domain, int type, int protocol);
    1. 头文件:#include <sys/types.h>、#include <sys/socket.h>
    2. domain:设置通信的协议(AF_INET:网络通信、AF_UNIX:域间套接)
    3. type:套接字类型(SOCK_STREAM:流式套接(TCP常用)、SOCK_DGRAM:面对数据报,提供数据报协议(UDP)、SOCK_RAM:原始套接)
    4. protocol:表示使用的那个(tcp/udp)协议,通过前面两个参数已经能确定是那个协议了,故可直接写成0

  1. int bind(int sockfd, const struct sockaddr *addr , socklen_t addrlen);
    1. 头文件:#include <sys/types.h>、#include<sys/socket.h>
    2. sockfd:创建的网络套接字
    3. addrlen:传入结构体的长度
    4. addr:
      sockaddr_in的结构,其中包括了:
    5. sin_family:AF_INET;//协议家族 ,绑定网络通信的信息
    6. sin_port
    7. s_addr


      所以对sockaddr_in local(addr)初始化写成(具体细节已注释):
cpp 复制代码
 //补充结构体sockaddr_in:
 bzero(&local,sizeof(local));//将指定的空间的大小内存清零
 
 local.sin_family = AF_INET;//协议家族 ,绑定网络通信的信息
 //sin:s:socket in:inet(IP地址!)
 local.sin_port = htons(_port);//将string的_port转换成网络序列,通过htons h:host、to、n:net、s:socket
 local_sin_addr.s_addr = inet_addr(_ip);//需要把uint16_t的ip转化成4字节的uint32_t的网络序列的ip,可以通过inet_addr

第一个参数:sin_family就是sockaddr_in将sa_prefix传进宏和family进行拼接得

,初始化成AF_INET(表示绑定网络通信)。

第二个参数:sin_port:初始化为_port主机转网络序列(用函数htons)。

第三个参数:sin_addr.s_addr:通过in_addr的结构知道还要对s_addr初始化成32位(4byte)。初始化为:把ip转换成32位并且网络序列化(直接用函数inet_addr)。

附:

函数:void bzero(void *s, size_t n)

作用:将指定内存全部清零、头文件:#include<strings.h>


接收用户信息函数:

cpp 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  1. sockfd:创建的sock文件
  2. buf:缓冲区(获取用户发来的信息)
  3. len:期望大小(返回值才是实际大小)
  4. flags:收数据的模式(默认设为0,阻塞式收消息)
  5. src_addr:输出型参数,接收到发送端的套接字信息(ip、port)
  6. addrlen:传入结构体的长度

发送用户信息函数:

cpp 复制代码
	ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数和上面都一样(不过诉了),不同点:

  1. dest_addr:填写要发给的对象信息(服务器可以先通过接收到的发送端信息直接填写,而客户端则是将提前得知的服务器的信息填写进去)

6.1.1编写服务器和客户端的代码:

UdpServer.hpp:

cpp 复制代码
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<strings.h>

#include <sys/types.h>         
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include<cerrno>
#include<cstring>

#include"Comm.hpp"
#include"nocopy.hpp"
#include"Log.hpp"


using namespace std;

static const uint16_t defaultport = 8888; 
static const uint16_t defaultfd = -1; 
static const int defaultsize = 1024; 

class UdpServer : public nocopy
{
public:
    UdpServer(const string& ip,uint16_t port = defaultport)
    :_ip(ip),_port(port),_sockfd(defaultfd)
    {}
    void Init()
    {
        //1. 创建套接字,创建成功后会返回 a ffile descriptor ,出错返回-1
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);//SOCK_DGRAM : udp
        if(_sockfd < 0)//创建套接字失败
        {
            lg.LogMessage(Fatal,"socket errno, %d : %s\n",errno,strerror(errno));
            exit(Socket_Err);
        }
        lg.LogMessage(Info,"socket success,sockfd : %d\n",_sockfd);
    
        //2. 绑定,指定网络信息 相当于给套接字命名
        struct sockaddr_in local;//该结构体 需要新加头文件:#include <netinet/in.h>、#include <arpa/inet.h>
        
        //补充结构体sockaddr_in:
        bzero(&local,sizeof(local));//将指定的空间的大小内存清零
        local.sin_family = AF_INET;//协议家族 ,绑定网络通信的信息
        //sin:s:socket in:inet(IP地址!)
        local.sin_port = htons(_port);//将string的_port转换成网络序列,通过htons h:host、to、n:net、s:socket
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//需要把uint16_t的ip转化成4字节的uint32_t的网络序列的ip,可以通过inet_addr
        

//将刚刚设置好的网络信息和的sockfd文件信息进行绑定
        int n = ::bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n != 0)
        {
            lg.LogMessage(Fatal,"bind errno, %d : %s\n",errno,strerror(errno));
            exit(Bind_Err);
        }
//绑定完成后就:服务器就已经初始化完成了
    
    }
    void Start() 
    {
        char buffer[defaultsize];
        //服务器永远不退出!
        for(;;)
        { 
            //服务器进行udp收发消息
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);//socklen_t 就是无符号整形


            ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(n > 0)
            { 
                buffer[n] = 0;//当字符串
                cout << "client say# " << buffer << endl;
            
            //给对方发消息:
                sendto(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,len);
            
            }
        
        }
    }
    ~UdpServer()
    {}
private:
    string _ip;
    uint16_t _port;
    int _sockfd;
};

UdpClient.cc

cpp 复制代码
#include <iostream>

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include <cstring>
#include <unistd.h>
#include <cerrno>
using namespace std;

void Usage(const string &process)
{
    cout << "Usage: " << process << " server_ip server_ip" << endl;
}

//./udp_client server_ip server_port  向服务器发消息
int main(int argc, char **argv)
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    string serverip = argv[1]; // 字符串的点分十进制
    uint16_t serverport = stoi(argv[2]);

    // 1. 创建套接字,创建文件信息
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        cerr << "socket error: " << strerror(errno) << endl;
        return 2;
    }
    cout << "create socket success: " << sock << endl;
    // 2. 绑定
    // 其中客户端的socket也要绑定,但是不需要显示bind(client会在首次发送数据的时候会自动进行bind)
    // 不推荐自己绑定,为什么?服务器的端口号一定是众所周知的,不可改变。而client 需要 port,让其随机bind端口
    // client一般不绑定一个确定的socket因为:client 会非常多(打开如何软件),可能会导致别人占了你的端口
    // 就导致无法启动


    // 2.1 填充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)
    {
        string inbuffer;
        cout << "Please Enter# ";
        getline(cin, inbuffer);

        // 发给:
        int n = sendto(sock,inbuffer.c_str(),inbuffer.size(),0,(struct sockaddr*)&server,sizeof(server));
        if(n>0)//发送成功
        {
            char buffer[1024];
            //收消息
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            ssize_t m = recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);//虽然没用但也是要填的temp
            if(m > 0)
            {
                buffer[m] = 0;
                cout << "server echo# " << buffer << endl;
            }
            else 
                break;
        }
    }   

    close(sock); // 把文件描述符关掉
    return 0;
}

nerstat -anup:查看网络连接,网络情况的指令

-a:所有信息

n:num:将主机名转换为数字化

p:process信息

u:udp

当把ip设置为127.0.0.1表示在当前主机内进行本地通信

再对代码进行优化,当客户端发来信息时显示客户端的socket:

cpp 复制代码
//...
class UdpServer : public nocopy
{
public:
    UdpServer(const string& ip,uint16_t port = defaultport)
    :_ip(ip),_port(port),_sockfd(defaultfd)
    {}
    void Init()
    {
		//...
    }
    void Start() 
    {
        char buffer[defaultsize];
        //服务器永远不退出!
        for(;;)
        { 
            //服务器进行udp收发消息
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);//socklen_t 就是无符号整形

            ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(n > 0)
            { 
                InetAddr addr(peer);//新写一个类,对类进行封装

                buffer[n] = 0;//当字符串

                cout << "[" << addr.PrintDebug() << "]# "<<  buffer << endl;//调用该类中类函数
           
            //给对方发消息:
                sendto(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,len);
            
            }
        }
    }
    ~UdpServer()
    {}
private:
    string _ip;
    uint16_t _port;
    int _sockfd;
};

InetAddr.hpp:

cpp 复制代码
#pragma once
#include<iostream>
#include<string>
#include <sys/types.h>         
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<unistd.h>

using namespace std;

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr)
    {
        port = ntohs(addr.sin_port);
        ip = inet_ntoa(addr.sin_addr);//将网络序列转化成addr
    }
    string IP()
    {
        return ip;
    }
    uint16_t Port()
    {
        return port;
    }

    string PrintDebug()
    {
        string info = ip;
        info += ":";
        info += to_string(port);
        return info;
    }

private:
    string ip;
    uint16_t port;
};

云服务器公网IP其实是供应商给你虚拟出来的公网ip,无法bind

如果你是真的Linux环境,可以直接bind你的ip,但强烈不建议给服务器bind固定ip(这样服务器和特定ip绑死后就只能收到该绑定的机器的信息),更推荐本地任意ip绑定的方式:

所以我们能修改UdpServer.hpp的代码:

把UdpServer的成员变量ip取消掉(local.sin_addr.s_addr = inet_addr(_ip.c_str());),并且把ip绑定成INADDR_ANY(0,local.sin_addr.s_addr = INADDR_ANY;)

cpp 复制代码
//...

// 优化代码不绑定特定的ip
class UdpServer : public nocopy
{
public:
    UdpServer(uint16_t port = defaultport)
    :_port(port),_sockfd(defaultfd)
    {}
    void Init()
    {
//...

        local.sin_addr.s_addr = INADDR_ANY;//一般不固定一个ip,而是任意生成一个ip的动态绑定:INADDR_ANY == 0       
        // local.sin_addr.s_addr = inet_addr(_ip.c_str());//需要把uint16_t的ip转化成4字节的uint32_t的网络序列的ip,可以通过inet_addr

       
//...
    }
    void Start() 
    {
//...
    }
    ~UdpServer()
    {}
private:
    // string _ip;
    uint16_t _port;
    int _sockfd;
};

Makefile:

主函数:

cpp 复制代码
#include"UdpServer.hpp"
#include<memory>
#include<string>
#include"Comm.hpp"
using namespace std;

void Usage(string proc)
{
    cout << "Usag : \n\t" << proc << " local_ip local_port\n" << endl;
}

//./udp_sv 8888
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    string ip = argv[1];
    uint16_t port = stoi(argv[2]);

    unique_ptr<UdpServer> usvr = make_unique<UdpServer>(port);
    
    usvr->Init();
    usvr->Start();

    return 0;
}

Makefile:

bash 复制代码
.PHONY:all
all:udp_server udp_client

udp_server:Main.cc
	g++ -o $@ $^ -std=c++14

udp_client:UdpClient.cc
	g++ -o $@ $^ -std=c++14

.PHONY:clean
clean:
	rm -f udp_server udp_client

其中服务器的ip为0.0.0.0表示任意ip地址绑定

可以通过在别的客户端上对该服务器进行发送消息,只需要知道当前服务器的ip和端口,并且有当前客户端的可执行程序

云服务器中大部分端口号是被腾讯云、阿里云...拦截的。

我要能够进行数据的发送,需要开放指定的多个端口 8080、8081、...

6.2TCP协议

除UDP以学的外,主要新增有:

在socket函数创建文件时,第二个参数使用SOCK_STREAM(提供序列化的、可靠的、双方的、建立链接的字节流)

cpp 复制代码
int sockfd = socket(AF_INET,SOCK_STREAM,0);
  1. Server端:c/s双方要进行通信,得先建立连接(接收client的连接):
cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  1. sockfd sock文件
  2. addr 和 addrlen (与recvfrom中的参数一样)
  3. 头文件:#include <sys/types.h>、#include <sys/socket.h>
  4. 返回值:成功后会返回一个新增的文件描述符accepted socket,失败返回-1并设置错误码,而该返回的文件描述符sockfd就是用来将server获取新链接(sockfd在该内部进行服务的处理,我们创建的listensock只是用来找到client的!)

用来侦听功能将套接字置于侦听传入连接的状态:

cpp 复制代码
int listen(int sockfd, int backlog);

sockfd:sock文件

(backlog后面会具体写暂时只要知道在tcpServer中肯定要写)


  1. Client端:连接到服务器
cpp 复制代码
 int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  1. 头文件:#include <sys/types.h>、#include <sys/socket.h>

  2. 成功返回0,否则返回-1,错误码被设置

  3. 对于客户端来说并不需要自己绑定(bind)ip和port,client系统会随机一个端口! 当我们在发起连接的时候,client会被OS自动进行本地绑定

inet_pton 和 inet_addr类似都能把四字节的字符串ip转化成网络序列:

cpp 复制代码
int inet_pton(int af, const char *src, void *dst);
  1. af:通信的协议(AF_INET网络通信)
  2. src:来源(写ip)
  3. dst:目的的(写&local.sin_addr)
  4. 头文件:#include <arpa/inet.h>

6.2.1基本tcp实现通信:

Client.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <stdio.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> 


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

using namespace std;

int main(int argc , char*argv[])
{
    if(argc != 3)
    {
        cout << "Usag: \n\t"<< argv[0] << " " << "local_ip local_port \n" << endl;
        return Usage_Err;
    }  

    string serverip = argv[1];
    int serverport = atoi(argv[2]);

//1. 创建套接字socket
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        lg.LogMessage(Fatal,"create sock errorr, %d : %s\n",errno,strerror(errno));
        exit(Fatal);
    }

//2. connect
//不用bind , 系统在local的时候会自动bind
    struct sockaddr_in local;
    memset(&local,0,sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(serverport); 
    inet_pton(AF_INET,serverip.c_str(),&local.sin_addr);


    int n = connect(sockfd,CONV(&local),sizeof(local));
    if(n < 0)
    {
        cout << "connect error" << endl;
        return 2;
    }

    while(true)
    {
        string inbuffer;
        cout << "Pleace Enter# ";
        getline(cin,inbuffer);
        ssize_t n = write(sockfd,inbuffer.c_str(),inbuffer.size());//直接使用sockfd
        if(n > 0)
        {
            char buffer[1024];
            ssize_t m = read(sockfd,buffer,sizeof(buffer)-1);//直接使用sockfd
            if(m > 0)
            {
                buffer[m] = 0;//将最后位置写个0,作为结束标志 
                cout << "get a echo message -> " << buffer << endl;
            }
            else{
                break;
            }
        }
        else
        {
            cout << "write error" << endl;
            break;
        }

    }
    close(sockfd);
    return 0;
}

Server.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> 

#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"

static const int default_backlog = 5;

class TcpServer : public nocopy
{
public:
    TcpServer(uint16_t port):_port(port),_isrunning(false)
    {}

    void Init()
    {
        //创建sock文件
        _listensock = socket(AF_INET,SOCK_STREAM,0);
        if(_listensock < 0)
        {
            lg.LogMessage(Fatal,"create sock errorr, %d : %s\n",errno,strerror(errno));
            exit(Fatal);
        }   
        lg.LogMessage(Debug,"create sock success, sock:%d\n",_listensock);
        
        //填充本地信息并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 = htonl(INADDR_ANY);//0 

        //bind sock文件和网络序列
        if(bind(_listensock,CONV(&local),sizeof(local)) != 0)
        {
            lg.LogMessage(Fatal,"bind error, %d : %s\n",errno,strerror(errno));
            exit(Bind_Err);
        }
        lg.LogMessage(Debug,"bind success,sock: %d\n",_listensock);


        //设置socket为监听模式,tcp特有的
        if(listen(_listensock,default_backlog) != 0)
        {
            lg.LogMessage(Fatal,"listen error, %d : %s\n",errno,strerror(errno));
            exit(Listen_Err);
        }
        lg.LogMessage(Debug,"listen success,sock: %d\n",_listensock);
    }

//Tcp连接全双工通信
    void Service(int sockfd)
    {
        char buffer[1024];

        while(true)
        {
            ssize_t n = read(sockfd,buffer,sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;
                cout << "client echo# " << buffer << endl;

                string echo_string = "server echo# ";
                echo_string += buffer;
                write(sockfd,echo_string.c_str(),echo_string.size());
            }
            else if(n == 0)
            {
                lg.LogMessage(Info,"client quit...\n");
                break;
            }
            else
            {
                lg.LogMessage(Error,"listen error, %d : %s\n",errno,strerror(errno));
                break;
            }
        }
    }

    void Start()
    {
        _isrunning = true;
        while(_isrunning)//_listensock相当于是负责拉客的人,找到客人返回的文件描述符
        {
            //获取连接
            struct sockaddr peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensock,CONV(&peer),&len);//sockfd才是提供服务的服务员
            if(sockfd < 0)
            {
                lg.LogMessage(Fatal,"accept fail,%d : %s\n",errno,strerror(errno));
                continue;
            }
            lg.LogMessage(Debug,"accept success,sock: %d\n",_listensock);

            //提供服务: v1 ~ v4    
            Service(sockfd);
            close(sockfd);

        }

    }

    ~TcpServer()
    {}
    
private:    
    int _listensock;
    uint16_t _port;

    bool _isrunning;
};

Makefile:

bash 复制代码
.PHONY:all
all:tcp_server tcp_client

tcp_server:Main.cc
	g++ -o $@ $^ -std=c++14 -lpthread

tcp_client:TcpClient.cc
	g++ -o $@ $^ -std=c++14 -lpthread

.PHONY:clean
clean:
	rm -f tcp_server tcp_client

Main.cc

此处要通过Main.cc 启动服务器

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>
#include <stdio.h>

#include "TcpServer.hpp"
#include "Comm.hpp"

using namespace std;

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        cout << "Usag: \n\t"<< argv[0] << " " << "local_port\n" << endl;
        return Usage_Err;
    }
    
    uint16_t port = stoi(argv[1]);
    unique_ptr<TcpServer> tsvr(new TcpServer(port)); // 另外的使用方式
    //unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port);
    tsvr->Init();
    tsvr->Start();

    return 0;
}

测试结果:

通过netstat -nltp可以查看已有的网络链接

  1. IO类函数,write/read其实他们已经做了转网络序列
  2. udp:用户数据报,数据都是一个个报文的发送、tcp:面向字节流数据,一次发一条数据,这些数据可能是一个报文也可能是不完整的报文,后面通过边界来找到对应的
  3. sendto、recvfrom -> sendto 发了一次,一定对应对端recvfrom一次,面向数据报(类似于发邮件、快递一件一件的!)
  4. write -> 写了1,10,...次 ,接收方read可能一次就全部收完了,也可能多次,无论多少次,和对方发的无关,面向字节流 (类似于使用的自来水,用多少取决于自己)
  5. 我们的网络服务,不能在bash中以前台进程的方式运行,真正的服务器必须在Linux后台,以 守护进程(精灵进程)的方式持续不断的运行!

守护进程

会话与进程组

查看已用进程(查看sleep,通过ps查看在通过管道grep和head过滤出进程sleep和ps的第一行 )

powershell 复制代码
ps -ajx | head -1 && ps ajx | grep sleep

同时创建的进程会在同一个会话中,其中第一个创建的进程组id(和他的pid一样)和会话id就是所有进程的进程组id和会话id:

每次登录Linux -> OS 会给登录用户提供:

  1. bash
  2. 提供一个终端(给用户提供命令行解析->叫做一个会话)
  3. 在命令行中启动的所有的进程,最终默认都是在当前会话内部的一个进程组(可以是一个进程自成进程组)

    任何时刻,一个会话内部,可以存在很多个进程组(用户级任务),但是默认如何时刻,只允许一个进程组在前台(前台进程组)
    通过在启动进程是 加 & 让其进程组变成后台进程

    查看后台进程:
powershell 复制代码
jobs

其中第一列的序号就是后台进程的编号

将后台进程放回前台:

powershell 复制代码
fg + 后台进程编号

其中注意的是:
bash也是一个进程组!
而一个会话中只能有一个进程组在前台!

所以当sleep的进程组从后台到前台后bash就无法使用了,而前台是和终端和键盘相关,可以通过键位再把当前进程组放回后台(直接发送终止信号终止进程):

powershell 复制代码
ctrl + z


此时bash会自动被放回前台,并且发现其状态变成了Stopped,所以需要再把状态调回Runing:

powershell 复制代码
bg + 进程组编号

用户级任务 Vs 进程组:它们是一个概念,只不过进程组更加的专业化,进程组就是实现现有的任务。

会话:相当于开的一个窗口,其前台进程一开始就是bash,若要再创建进程就是在该会话内创建。


守护进程的作用以及实现原理

若想要服务器,不受用户登录和注销的影响:
就只能使用守护进程:它就是一个独立的会话,不隶属于任何bash的会话!将前面写的tcpserver、udpserver守护进程化,进程守护话后表示:该服务即使会话关闭后也仍然存在,也就相当于一个后台服务(24h的云服务器)

  1. 创建一个会话,并且设置自己为会话的首进程,这样会话id和进程组id与该进程的会话id相同
  2. 若要创建一个新的会话,调用进程不能是一个组长
    组长一般是多个进程的第一个,若只有一个进程,自成进程组,那组长自然就是自己
    所以要:创建子进程(fork),再让父进程终止(这样子进程就既不是组长了,并且会话也只有一个进程),所以守护进程就一定是孤儿进程(系统是父进程 ppid:1)
  3. 因为守护进程和当前目录无关所以可以把其目录改成根目录(当然也能不进行修改)

    4. 对于守护进程不需要和用户进行输入输出,错误的关联了,所以就能关闭或者重定向到null文件,因为写到null文件(null字符设备)中的都会被丢弃,凡是从null文件中拿的都是空,该文件目录为:/dev/null

模拟实现守护进程:

cpp 复制代码
#pragma once

//Daemon 守护、精灵
#include <signal.h>
#include <unistd.h>
#include <cstdlib>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const char* root = "/";
const char* dev_null = "/dev/null";


//守护进程的核心5个步骤
void Daemon(bool ischdir,bool isclose)
{
//1. 忽略可能出错的信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);

//2. 关闭父进程
    if(fork() > 0) exit(0);

//3. 变成组长
    setsid();

//4. 每一个进程都有自己的cwd,是否将当前进程cwd改成 / 根目录
    if(ischdir) 
        chdir(root);


//5. 已经变成守护进程了,就不需要和用户进行输入输出,错误的关联了
    if(isclose)
    {
        close(0);
        close(1);
        close(2);
    }
    else//一般就直接把他们重定向到O_RDWR
    {
        int fd = open(dev_null,O_RDWR);
        if(fd > 0)//文件打开成功
        {
            dup2(fd,0);
            dup2(fd,1);
            dup2(fd,2);
            close(fd);
        }
    }
   
}

//main.cc
#include "Daemon.hpp"
#include <unistd.h>

int main()
{

//变成守护进程
    Daemon(true,false);

    while(true)
    {
//要执行的核心代码
    }

    return 0;
}

创建好的守护进程:

系统也是有提供把进程守护进程化的方法的:

cpp 复制代码
#include <unistd.h>

int daemon(int nochdir, int noclose);

和上面模拟写的类似,但传的参数不一样内部实现也若有不同:

  1. 如果nochdir为零,daemon()将调用进程的当前工作目录更改为根目录("/");否则,当前工作目录保持不变。
  2. 如果noclose为零,daemon()将标准输入、标准输出和标准错误重定向到/dev/null;否则,不会对这些文件描述符进行任何更改。

TCP通信协议过程

在tcp客户端和服务端连接采用:三次握手也就是进行三次的报文交换(SYN、SYN+ACK、ACK),当三次握手完成后accept才会返回。connect(一个系统调用)只是发起了第一次握手,三次握手的本质是建立共识!

  1. connect和accept都会阻塞式的等待三次握手的完成
  2. 三次握手是建立链接前必须做的

在tcp客户端和服务端断开链接采用:一次close(关闭客户端->服务器通信信道)对应着两次挥手(发送FIN,ACK报文),所以两次close(关闭-服务器>客户端通信信道)对应着四次挥手,因为是全双工通信,所以两方都需要要关闭信道

所以tcp建立链接需要三次握手,断开链接需要四次挥手


本章完。预知后事如何,暂听下回分解。

如果有任何问题欢迎讨论哈!

如果觉得这篇文章对你有所帮助的话点点赞吧!

持续更新大量计算机网络细致内容,早关注不迷路。

相关推荐
安步当歌1 小时前
【WebRTC】视频发送链路中类的简单分析(下)
网络·音视频·webrtc·视频编解码·video-codec
米饭是菜qy1 小时前
TCP 三次握手意义及为什么是三次握手
服务器·网络·tcp/ip
yaoxin5211231 小时前
第十九章 TCP 客户端 服务器通信 - 数据包模式
服务器·网络·tcp/ip
鹿鸣天涯2 小时前
‌华为交换机在Spine-Leaf架构中的使用场景
运维·服务器·网络
星海幻影2 小时前
网络基础-超文本协议与内外网划分(超长版)
服务器·网络·安全
WeeJot嵌入式2 小时前
网络百问百答(一)
网络
湖南罗泽南2 小时前
p2p网络介绍
网络·网络协议·p2p
Michael_Good2 小时前
【计算机网络】设备如何监听 ARP 请求广播
计算机网络
IPdodo全球网络3 小时前
解析“ChatGPT网络错误”:从网络专线到IP地址的根源与解决方案
网络·tcp/ip·chatgpt
腾科张老师4 小时前
为什么要使用Ansible实现Linux管理自动化?
linux·网络·学习·自动化·ansible