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

news2024/12/25 14:14:15

绪论​
“宿命论是那些缺乏意志力的弱者的借口。 ——罗曼.罗兰”,本章是为应用层打基础,因为在写应用层时将直接通过文本和代码的形式来更加可视化的理解网络,本章主要写的是如何使用网络套接字和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的结构,其中包括了:
    1. sin_family:AF_INET;//协议家族 ,绑定网络通信的信息
    2. sin_port
    3. s_addr
    在这里插入图片描述
    在这里插入图片描述
    所以对sockaddr_in local(addr)初始化写成(具体细节已注释):
 //补充结构体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>


接收用户信息函数:

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:传入结构体的长度

发送用户信息函数:

	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:

#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:

#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:

//...
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:

#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;)

//...

// 优化代码不绑定特定的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:
主函数:

#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:

.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(提供序列化的、可靠的、双方的、建立链接的字节流)

int sockfd = socket(AF_INET,SOCK_STREAM,0);
  1. Server端:c/s双方要进行通信,得先建立连接(接收client的连接):
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的!

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

int listen(int sockfd, int backlog);

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


  1. Client端:连接到服务器
 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转化成网络序列:

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

#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

#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:

.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 启动服务器

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

在这里插入图片描述

守护进程

会话与进程组

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

ps -ajx | head -1 && ps ajx | grep sleep

在这里插入图片描述同时创建的进程会在同一个会话中,其中第一个创建的进程组id(和他的pid一样)和会话id就是所有进程的进程组id和会话id:
在这里插入图片描述

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

  1. bash
  2. 提供一个终端(给用户提供命令行解析->叫做一个会话)
  3. 在命令行中启动的所有的进程,最终默认都是在当前会话内部的一个进程组(可以是一个进程自成进程组)
    在这里插入图片描述
    任何时刻,一个会话内部,可以存在很多个进程组(用户级任务),但是默认如何时刻,只允许一个进程组在前台(前台进程组)
    通过在启动进程是 加 & 让其进程组变成后台进程
    在这里插入图片描述
    查看后台进程
jobs

在这里插入图片描述
其中第一列的序号就是后台进程的编号
在这里插入图片描述
将后台进程放回前台

fg + 后台进程编号

在这里插入图片描述
其中注意的是:
bash也是一个进程组!
而一个会话中只能有一个进程组在前台!

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

ctrl + z

在这里插入图片描述
此时bash会自动被放回前台,并且发现其状态变成了Stopped,所以需要再把状态调回Runing

bg + 进程组编号

在这里插入图片描述
用户级任务 Vs 进程组:它们是一个概念,只不过进程组更加的专业化,进程组就是实现现有的任务。
在这里插入图片描述

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


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

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

  1. 创建一个会话,并且设置自己为会话的首进程,这样会话id和进程组id与该进程的会话id相同
  2. 若要创建一个新的会话,调用进程不能是一个组长
    组长一般是多个进程的第一个,若只有一个进程,自成进程组,那组长自然就是自己
    所以要:创建子进程(fork),再让父进程终止(这样子进程就既不是组长了,并且会话也只有一个进程),所以守护进程就一定是孤儿进程(系统是父进程 ppid:1)
  3. 因为守护进程和当前目录无关所以可以把其目录改成根目录(当然也能不进行修改)
    在这里插入图片描述
    4. 对于守护进程不需要和用户进行输入输出,错误的关联了,所以就能关闭或者重定向到null文件,因为写到null文件(null字符设备)中的都会被丢弃,凡是从null文件中拿的都是空,该文件目录为:/dev/null
    在这里插入图片描述

模拟实现守护进程:

#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;
}

创建好的守护进程:
在这里插入图片描述
系统也是有提供把进程守护进程化的方法的:

#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建立链接需要三次握手,断开链接需要四次挥手


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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1983649.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“论负载均衡技术在Web系统中的应用”写作框架软考高级论文系统架构设计师论文

论文真题 负载均衡技术是提升Web系统性能的重要方法。利用负载均衡技术&#xff0c; 可将负载(工作任务) 进行平衡、分摊到多个操作单元上执行&#xff0c; 从而协同完成工作任务&#xff0c; 达到提升Web系统性能的目的。 请围绕“负载均衡技术在Web系统中的应用”论题&…

云原生真机实验

基于Proxmox VE构建中小企业云计算平台 首先Proxmox VE是什么&#xff1f;能用来做什么&#xff1f; Proxmox VE是一个完整的企业虚拟化开源平台。借助内置的 Web 界面&#xff0c;可以在单个解决方案上轻松管理 VM(开虚拟机的) 和容器、软件定义的存储和网络、高可用性群集以…

使用Python连接华为云物联网服务器与服务器完成数据交互

一、前言 随着物联网技术的快速发展&#xff0c;越来越多的设备和系统需要通过网络进行连接和数据交换&#xff0c;以实现智能化管理和控制。华为云物联网平台作为业界领先的物联网解决方案提供商&#xff0c;提供了稳定可靠的MQTT服务器&#xff0c;使得设备能够轻松接入云端…

数据结构(其四)--特殊矩阵的存储

目录 11.特殊矩阵的压缩存储 &#xff08;1&#xff09;.一维数组的储存结构 &#xff08;2&#xff09;.二维数组的存储结构 &#xff08;3&#xff09;.普通矩阵的存储 &#xff08;4&#xff09;.特殊矩阵的压缩存储 i.对称矩阵 ii.三角矩阵 iii.三对角矩阵 iiii.稀疏矩…

上位机 OPC协议、KepServerEX

OPC 全称 OLE For Process Control 》》OPC&#xff08;Open Platform Communications&#xff0c;以前称为 OLE for Process Control&#xff09;是一组软件技术 opc出现之前&#xff0c;软件和硬件是分开的&#xff0c; 如果要与不同的设备通信&#xff0c;需要用各个厂商的…

DepthB2R【附代码】(权限提升)

靶机下载地址&#xff1a; https://www.vulnhub.com/entry/depth-1,213/https://www.vulnhub.com/entry/depth-1,213/ 1. 主机发现端口扫描目录扫描 1.1. 主机发现 nmap -sn 192.168.43.0/24|grep -B 2 08:00:27:08:B4:07 1.2. 端口扫描 nmap -p- 192.168.43.112 1.3. 目录…

NoSQL 数据库之MongoDB

MongoDB 是一个开源的 NoSQL 数据库&#xff0c;由 MongoDB Inc. 研发和维护。它采用文档存储模型&#xff0c;使用 JSON 类似的 BSON&#xff08;二进制 JSON&#xff09;格式来存储数据。MongoDB 具有高性能、易扩展和高可用性等特点&#xff0c;广泛应用于现代 web 应用程序…

Linux学习笔记:iptables命令管理

1、iptables简介 其实iptables只是Linux防火墙的管理工具而已&#xff0c;位于/sbin/iptables。真正实现防火墙功能的是netfilter&#xff0c;它是Linux内核中实现包过滤的内部结构。 语法格式&#xff1a;iptables [-t table] COMMAND [chain] CRETIRIA -j ACTION -t&#…

sqllabs通关

sqllabs5:(报错注入) ?id1 回显You are in........... ?id2-1 回显You are in........... ?id1 回显 1 LIMIT 0,1 判断是字符型&#xff0c;闭合。?id1order by 3-- //页面显示正常我们试了4行得出是报错注入 我们先爆库名 http://127.0.0.1/sqli-labs-master/L…

技术详解:视频美颜SDK与直播美颜插件开发指南

本篇文章&#xff0c;小编将详细探讨如何开发视频美颜SDK以及如何将其集成到直播应用中。 一、视频美颜SDK的基本原理 视频美颜SDK其实现的基本步骤如下&#xff1a; 1.图像采集与预处理&#xff1a;从相机或视频流中获取原始图像帧&#xff0c;进行必要的预处理如色彩空间转…

IoTDB 入门教程 基础篇②——IoTDB 企业版比开源版本值在哪?

文章目录 一、前文二、功能对比三、可视化控制台四、白名单五、审计日志六、数据备份七、机器学习八、总结 一、前文 IoTDB入门教程——导读 二、功能对比 由天谋科技官网得知&#xff0c;IoTDB&#xff08;开源版&#xff09;与TimechoDB&#xff08;企业版&#xff09;的功能…

Android Studio Gradle多渠道打包

原理使用Android Studio打一次渠道包&#xff0c;用反编译工具反编译后&#xff0c;修改渠道信息重新编译 准备文件 分渠道配置文件&#xff1a;channel.txt ↓ # 多渠道配置里“统计平台”、“市场名称”、“渠道编号”分别代表什么意思&#xff1f; # 统计平台&#xff1a;…

Java 后端接收HTML等标签数据,到后端标签丢失

文章目录 前言一、修改Xss配置总结 前言 一开始以为是接收参数出了问题&#xff0c;后面看了RequestBody注解并不会改变参数&#xff0c; 最后发现是xss的配置问题。 一、修改Xss配置 把enabled: true改成false就好了 #xss配置,防止xss攻击 xss:#过滤开关&#xff1a;enable…

简单的docker学习 第10章 docker管理监控平台

第10章 Docker管理监控平台 当 Docker引擎中管理的镜像、容器、网络等对象数量变得越来越多时&#xff0c;通过简单的 docker命令来管理已经显得使人力不从心了。于是就出现了很多的 Docker 可视化管理平台。我们这里对现在较流行的、使用较多的几种平台进行介绍。 10.1 Dock…

【Python】torch.nn模块中函数详解和示例(一)

前言 在深度学习日益成为解决复杂问题重要工具的今天&#xff0c;PyTorch凭借其灵活性和易用性&#xff0c;成为了众多研究者与开发者的首选框架。本系列博客 将对torch中的nn模块中186个函数进行介绍&#xff0c;以函数首字母从a到z的排序开展&#xff0c;包含函数原理、原型…

【css】 CSS3+JS做一个酷炫的仪表进度条3d进度条

创建一个动态进度环组件 在现代网页设计中&#xff0c;进度环是一种常见的视觉元素&#xff0c;用于展示任务的完成度或加载状态。本文将介绍如何使用Vue.js和Less创建一个动态进度环组件&#xff0c;该组件不仅具有美观的视觉效果&#xff0c;还能够根据用户输入动态改变颜色…

「链表」Floyd判环法(弗洛伊德判圈法|龟兔赛跑法)/ LeetCode 141(C++)

给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&#xff08;…

【C++】C++11的新特性 — function 包装器 , bind包装器

有些人的生活&#xff0c;可以轻轻松松&#xff0c;有些人的生活就是奥运会&#xff0c;生下来就在跑道上&#xff0c;如果不去全力奔跑&#xff0c;注定会被淘汰&#xff0c;更何况&#xff0c;即使努力奔跑&#xff0c;也未必能战胜很多人。 -- 傅首尔 -- C11的新特性 1 fun…

判断字符串是否接近:深入解析及优化【字符串、哈希表、优化过程】

本文将详细解析解决这个问题的思路&#xff0c;并逐步优化实现方案。 问题描述 给定两个字符串 word1 和 word2&#xff0c;如果通过以下操作可以将 word1 转换为 word2&#xff0c;则认为它们是接近的&#xff1a; 交换任意两个现有字符。将一个现有字符的每次出现转换为另…