半亩花草 发表于 2024-9-1 19:41:45

Linux 网络套接字剖析:实现网络通讯

https://i-blog.csdnimg.cn/direct/d24dd64ef43949a0a60ee969f0c90d13.png


一.网络底子

1.协议

计算机相关的硬件软件很多,这时就需要制定一个尺度保障数据的传输,所以网络协议就是是计算机网络中通讯双方遵照的约定。本质上来说:协议是通过布局体表达出来的特定的双方都熟悉的布局体对象。
2.OSI与TCP/IP模子

为了更好的将网络维护起来,OSI将网络模子分成了七层的理论模子,TCP/IP为互联网的实际模子通常为五层(四层)。
TCP/IP模子:

[*]物理层: 负责光/电信号的传递方式. 好比现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在重要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念
[*]数据链路层:负责处理硬件层面的通讯细节,包括怎样在本地网络上发送和吸收数据。它涵盖了物理层和数据链路层的功能,管理数据帧的传输、错误检测、以及链路控制。
[*]网络层:重要负责数据包的路由和转发,确保数据可以或许在差别网络之间传输。这一层定义了IP地点的布局和数据包的传输路径。IP协议(IPv4和IPv6)是这一层的核心协议。
[*]传输层:提供端到端的数据传输服务,确保数据的完整性和顺序性。传输层负责数据的分段、传输、重组,并根据需要提供错误检测和流量控制。TCP和UDP协议在这一层运行,分别提供可靠的连接型和高效的无连接型数据传输服务。
[*]应用层:直接为用户提供网络服务,负责数据的格式化、加密以及应用程序之间的通讯。这一层涵盖了OSI模子的应用层、体现层和会话层功能,是用户与网络交互的接口。
网络协议栈体现图:
https://i-blog.csdnimg.cn/direct/bd558d642d4e4de2a53c868105f57f8e.png
3.网络通讯流程

两主机通过TCP/IP通讯过程所示
https://i-blog.csdnimg.cn/direct/a71b0f20dca947dcab6e074f21c93bab.png
发送数据在不断的对数据加上报头,发送数据即封装的过程,接受数据是在不断的解包和分用的过程。分用:决定将自己的有用在和交付给上层的哪个协议的能力(就是分用)。传输层的叫数据段,网络层的叫叫数据报,数据链路层叫数据帧,通讯的的过程本质就是不断封装和解包的过程,报文 = 报头 + 有用载荷。
局域网通讯原理是基于碰撞域和碰撞检测的,互换机会划分碰撞域,局域网可以当作多台主机所共享的临界资源,所以是要保证互斥的。
4.IP与Mac地点

在计算机网络中,IP地点和MAC地点是两种用于标识设备的紧张地点范例。
IP地点:
IP地点(是分配给网络中每个设备的逻辑地点,用于在网络层举行通讯。IP地点有两种版本:IPv4和IPv6。


[*]IPv4地点: 由32位二进制数构成,通常体现为四个十进制数(例如,192.168.1.1),每个数值代表8位(一个字节)。
[*]IPv6地点: 由于IPv4地点枯竭问题,IPv6应运而生,它使用128位地点空间,体现为8组16进制数(例如,2001:0db8:85a3:0000:0000:8a2e:0370:7334)。
Mac地点:
MAC地点网络接口卡(网卡)的唯一标识符,存在于数据链路层。每个网络设备出厂时都会分配一个唯一的MAC地点,通常由硬件制造商确定。MAC地点由48位二进制数构成,6字节
MAC地点用于在同一局域网(LAN)内的设备之间举行通讯。因为它是硬件地点,所以在设备移动到差别网络时保持不变。互换机等网络设备使用MAC地点来学习和识别局域网中设备的位置,并相应地转发数据帧。
在网络通讯中,当一个设备需要与另一个设备通讯时,通常需要知道对方的IP地点。然而,数据帧(数据链路层)的实际传输依靠于MAC地点。这时,ARP(地点剖析协议)发挥作用。
二.网络编程套接字

1.端口号

端口号为2字节16位的整数,范围为在0到65535之间,它与IP地点一起构成一个套接字(socket),唯一标识网络中的某个服务。用来标识唯一进程。
那么端口号和进程pid有什么区别呢?,端口号是用来标识网络服务需要用到的进程,不是全部进程都要通讯,但是全部进程都必须有pid标识自己。一个端口号可以被多个进程绑定,一个端口号不能绑定多个进程,在公网上:IP地点能体现唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程 ,所以用IP和端口号可以标识全网唯一的一个进程。
2.网络字节序

在计算机网络中,数据传输可能会在差别计算机系统上。由于差别系统可能使用差别的字节序),为了确保数据在网络中传输时的正确性,需要统一使用一种尺度的字节序。所以诞生了网络字节序:TCP/IP协议规定网络数据流应该采用大端字节序。所以如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
接着主机序列转网络序列的库函数应运而生:
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

h代表主机序列转网络序列,n代表网络字节序列转主机序列,其中如果机器是小端字节序会做相应的转换。
3.tcp、udp协议

在Linux网络编程中,TCP(传输控制协议)和UDP(用户数据报协议)是两种最常用的传输层协议。
UDP协议(用户数据报协议):


[*]无连接:UDP是无连接的协议。数据传输之前不需要建立连接,因此可以更快地发送数据,但不保证数据的可靠性。
[*]不可靠传输:UDP不保证数据包的到达顺序,也不提供数据包的确认或重传机制。如果数据包丢失,UDP不会重发。
[*]面向数据报:在计算机网络中,**数据报(Datagram)**是指独立传输的、具有独立完整意义的数据单元。
[*]传输层协议
TCP协议(传输控制协议):


[*]有连接:TCP是一种面向连接的协议。在数据传输之前,客户端和服务器需要通过三次握手建立连接,确保双方预备好举行通讯。
[*]面向字节省:TCP中,数据被看作是一个连续的字节省。发送方可以将任意数量的字节发送到吸收方,而吸收方会以流的形式吸收到这些字节。这意味着发送方发送的每一段数据不会被当作一个独立的数据单元,TCP不会将数据拆分成数据报。吸收方吸收到的数据流是发送方数据流的一个完整视图。
[*]可靠传输:TCP提供可靠的数据传输,通过数据包的确认和重传机制来确保数据完整无误。数据包的顺序也会被正确维护。
[*]传输层协议
4.socket编程

socket是一种通讯机制socket 是应用层与传输层之间的一个接口,通过它,应用程序可以使用底层网络协议来发送和吸收数据。网络协议栈中的Socket通常被用来实现TCP/IP协议通讯。
socket编程系列函数:
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
socket用来创建一个套接字:


[*]domain:指定协议族,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地通讯)。
[*]type:指定Socket范例,如 SOCK_STREAM(流式套接字,TCP)、SOCK_DGRAM(数据报套接字,UDP)。
[*]protocol:指定协议,一样寻常设置为 0,让系统选择符合的协议。
[*]乐成时返回一个Socket形貌符(非负整数),失败时返回 -1 并设置 errno。
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
bind函数在网络编程中用于将一个套接字(socket)绑定到一个特定的地点和端口(struct sockaddr布局体)。


[*]sockfd:socket函数返回的Socket形貌符。
[*]addr:指向struct sockaddr布局的指针,其中包含要绑定的地点信息。
[*]addrlen:addr布局的巨细。
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

listen 函数在网络编程中用于将一个已经绑定到地点和端口的套接字转换为监听状态,以便接受客户端的连接哀求。这个函数重要用于服务器端。


[*]sockfd:socket函数返回的Socket形貌符。
[*]backlog:指定连接哀求队列的最大长度。
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept 函数在网络编程中用于接受客户端的连接哀求。它重要用于服务器端,在一个已经处于监听状态的套接字上调用,用于从等待队列中提取一个连接哀求,并返回一个新的套接字形貌符用于与客户端举行数据互换。
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数在网络编程中用于发起与长途主机的连接。它重要用于客户端程序,试图连接到指定的服务器地点和端口。


[*]addr:阐明:指向一个 struct sockaddr 布局体的指针,包含了长途主机的地点和端口信息。
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                                struct sockaddr *src_addr, socklen_t *addrlen);

recvfrom 函数在网络编程中用于从一个套接字吸收数据,它特别实用于面向数据报的协议(如 UDP),但也可以用于其他协议。


[*]sockfd:由 socket 函数创建的套接字形貌符。这个套接字应该是用来吸收数据的,并且可以是任何支持数据吸收的协议(如 UDP)。
[*]buf:指向一个缓冲区的指针,用于存储吸收到的数据。这个缓冲区应该富足大,以容纳预期的数据量。
[*]len:缓冲区 buf 的巨细(以字节为单位)。吸收到的数据量不能超过这个巨细
[*]flags:吸收操作的标志。通常使用 0,但也可以使用以下标志之一:MSG_OOB:吸收带外数据(如果有)。MSG_PEEK:窥视数据,不从队列中移除。MSG_DONTWAIT:非阻塞模式(实用于非阻塞套接字)。
[*]src_addr:指向一个 struct sockaddr 布局体的指针,用于存储数据源的地点信息。对于 IPv4 通讯,通常使用 struct sockaddr_in;对于 IPv6 通讯,通常使用 struct sockaddr_in6。这个参数可以为 NULL,如果不需要源地点信息。
[*]addrlen:指向 socklen_t 范例变量的指针,体现 src_addr 布局体的巨细。在调用前应设置为 src_addr 的巨细;调用后,它将包含实际的地点长度。可以为 NULL,如果不需要源地点信息。
5.sockaddr布局剖析

在网络编程中,sockaddr 布局体是用于体现网络地点的底子布局体。常见变体有sockaddr_in 和 sockaddr_un 的具体剖析:
https://i-blog.csdnimg.cn/direct/391149cbff9544e7bb5acb639ad651e4.png


[*]sockaddr 布局:是一个通用的地点布局体,用于体现网络地点。具体地点信息存储在其派生布局体中。
[*]sockaddr_in:用于体现 IPv4 地点,包含地点族、端口号和 IPv4 地点。实用于使用 TCP/UDP 协议的通讯。
[*]sockaddr_un:用于体现UNIX 域套接字的本地通讯地点,包含地点族和路径名。实用于本地进程间通讯。
套接字分为三种:域间套接字(同一机器内)、原始套接字(网络工具)、网络套接字(用户间网络通讯)
6.实现Udp_socket

首先我们分别要实现客户端和服务器逻辑,让服务器接受客户端的数据,并且返回给客户端。
首先我们创建服务器类:
class UdpServer
{
private:
    int _socketid;//网络文件描述符
    string _ip;//字符串形式ip地址
    uint16_t _port;//服务器进程的端口号
};
构造函数:
UdpServer(uint16_t port = default_port, string ip = default_ip)
      :_socketid(0)
      ,_ip(ip)
      ,_port(port)
    {}
绑定ip缺省为0.0.0.0代表允许吸收来自任何IP地点的连接哀求
创建套接字和绑定sockaddr_in布局体:
void Init()
    {   //1.创建udp socket
      _socketid = socket(AF_INET,SOCK_DGRAM,0);
      if(_socketid <0)
      {   
            exit(Socket_err);
      }
      //2.bind
      //int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

      struct sockaddr_in _obj;
      bzero(&_obj,sizeof(_obj));

      _obj.sin_family = AF_INET;
      _obj.sin_port = htons(_port);
      _obj.sin_addr.s_addr = inet_addr(_ip.c_str());

      int ret = bind(_socketid,(struct sockaddr*)&_obj,sizeof(_obj));
      if(ret<0)
      {
            exit(Bind_err);
      }
    }

其中AF_INET代表IPV4协议,SOCK_DGRAM面向数据报的套接字,htons用于将主机字节序列的端口号转为网络序列,inet_addr将char*范例转换成in_addr_t范例,bind将套接字与存储好协议和ip与端口号的struct sockaddr_in绑定起来。bzero将指定内存块清空,常用于使用之前初始化布局体
接着举行客户端数据收发逻辑:
void run()
    {
      char Buffer;
      string temp;

      while(true)
      {
         //ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
         //struct sockaddr *src_addr, socklen_t *addrlen);

         struct sockaddr_in client;
         socklen_t len = sizeof(client);

         ssize_t Re_ret = recvfrom(_socketid,Buffer,sizeof(Buffer)-1,0,//接收
                (struct sockaddr*)&client,&len);
            if(Re_ret<0)
            {
                lg(Fatal,"bind create is error : %d",Re_ret);
                continue;
            }

            Buffer = 0;//看成字符串
            temp = Buffer;
            cout<<temp<<endl;
            //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      //const struct sockaddr *dest_addr, socklen_t addrlen);
            sendto(_socketid,temp.c_str(),temp.size(),0,(struct sockaddr*)&client,len);//发送
      }
    }
客户端逻辑:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;

void Usage(string proc)
{
    cout << "Usage: " << proc << " serverip serverport"<< endl;
}

// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(0);
    }
    string Ser_ip = argv;
    uint16_t Ser_proc = stoi(argv);
    //1.创建struct sockaddr
   
    struct sockaddr_in Server;
    bzero(&Server,sizeof(Server));

    Server.sin_family=AF_INET;
    Server.sin_port = htons(Ser_proc);
    Server.sin_addr.s_addr= inet_addr(Ser_ip.c_str());

    int _sockeid = socket(AF_INET,SOCK_DGRAM,0);
    if(_sockeid <0)
    {
      cout<<"Client is error "<<endl;
      exit(1);
    }

    char Buffer;
    string message;

    socklen_t len = sizeof(Server);

    while(true)
    {
      cout<<"Please enter :";
      getline(cin,message);
      sendto(_sockeid,message.c_str(),sizeof(message)-1,0,(struct sockaddr*)&Server,len);


      struct sockaddr_in temp;
      socklen_t len = sizeof(temp);

      ssize_t s = recvfrom(_sockeid, Buffer, sizeof(Buffer)-1, 0, (struct sockaddr*)&temp, &len);//接受&
      if(s > 0)
      {
            Buffer = 0;
               cout << Buffer << endl;
      }
    }
    close(_sockeid);
    return 0;
}
客户端也是需要bind的但是是由OS在客户端发送数据的时间自动做。不需要显示调用bind首先客户端想要与服务器通讯,要知道服务器的ip和端口号,这里用到了可变参数,让用户的运行的时间告诉进程服务器的ip和端口号。之后的逻辑乏善可陈,即是发送和接受数据。
我们再设置服务器端口号时,要知道0到1023:为系统内定的端口号,一样寻常都要有固定的。
https://i-blog.csdnimg.cn/direct/f5d0c539b6b547ff9edad95f7edf31ef.png
云云以来我们就封装出了udpsocket实现简单的cs通讯。
7.实现Windows与Linux通讯

将Linux机器作为服务器,代码逻辑与上相同:
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "Log.hpp"

using namespace std;
Log lg;

enum{
    SOCKET_ERR=1,
    BIND_ERR
};

uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer{
public:
    UdpServer(const uint16_t &port = defaultport, const string &ip = defaultip)
      :sockfd_(0),
      port_(port),
      ip_(ip),
      isrunning_(false)
    {}
    void Init()
    {
      sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
      if(sockfd_ < 0)
      {
            lg(Fatal, "socket create error, sockfd: %d", sockfd_);
            exit(SOCKET_ERR);
      }
      lg(Info, "socket create success, sockfd: %d", sockfd_);
      

      struct sockaddr_in local;
      bzero(&local, sizeof(local));
      local.sin_family = AF_INET;
      local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
      local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
      // local.sin_addr.s_addr = htonl(INADDR_ANY);

      if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
      {
            lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);
      }
      lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }

    void Run() // 对代码进行分层
    {
      isrunning_ = true;
      char inbuffer;
      string temp;
      while(isrunning_)
      {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
         

            inbuffer = 0;//看成字符串
            temp = inbuffer;
            cout<<temp<<endl;
            //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      //const struct sockaddr *dest_addr, socklen_t addrlen);
            sendto(sockfd_,temp.c_str(),temp.size(),0,(struct sockaddr*)&client,len);//发送
      }
    }
    ~UdpServer()
    {
      if(sockfd_>0) close(sockfd_);
    }
private:
    int sockfd_;   // 网路文件描述符
    string ip_; // 任意地址bind 0
    uint16_t port_;// 表明服务器进程的端口号
    bool isrunning_;

};
Windows下vs2022代码:
#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS


#include <stdio.h>
#include <winsock2.h>
#include <Windows.h>

#include<iostream>
#include<string>

#pragma comment(lib,"ws2_32.lib") //引入库文件

int main()
{
        //初始化网络环境
        WSADATA wsa;
        if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
        {
                printf("WSAStartup failed\n");
                return -1;
        }
        //建立一个udp的socket
        SOCKET socked = socket(AF_INET, SOCK_DGRAM, 0);
        if (socked == INVALID_SOCKET)
        {
                printf("create socket failed\n");
                return -1;
        }

        int port = 8080;
        std::string ip = "";//服务器ip地址

        //创建结构体
        sockaddr_in addr = { 0 };
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.S_un.S_addr = inet_addr(ip.c_str());

        std::string info;
        char buffer;
        memset(buffer, 0, sizeof(buffer));

        //收发数据
        while (true) {
                std::cout << "Please enter:";
                std::getline(std::cin, info);
                //发送数据
                int n = sendto(socked, info.c_str(), info.size(), 0, (SOCKADDR*)&addr, sizeof(SOCKADDR));
                if (n == 0)
                {
                        printf("send failed\n");
                        return -1;
                }
                sockaddr_in t = { 0 };
                int len = sizeof(sockaddr_in);
                // 接收数据
                n = recvfrom(socked, buffer, sizeof(buffer) - 1, 0, (SOCKADDR*)&t, &len);
                buffer = 0;

                std::cout << buffer << std::endl;
                memset(buffer, 0, sizeof(buffer));
        }

        //关闭SOCKET连接
        closesocket(socked);
        //清理网络环境
        WSACleanup();
        return 0;
}
https://i-blog.csdnimg.cn/direct/b104162847dd49238585a97eb94dac6c.png
即可完成通讯
8.Linux下长途执行指令

popen 函数允许程序通过一个文件流与外部下令举行交互:
FILE *popen(const char *command, const char *type);


[*]command:一个指向字符串的指针,体现要执行的下令。这可以是任何可以在下令行执行的下令,包括路径、可执行文件名以及下令的参数。
[*]type:一个指向字符串的指针,用于指定文件流的范例,可以是 “r” 或 “w”:“r”:体现读取(即从下令的尺度输出读取数据)。“w”:体现写入(即将数据写入到下令的尺度输入)。
[*]乐成时,返回一个指向 FILE 的指针,该指针可以用于读取或写入外部下令的输入输出。
也就是客户端向服务器发送数据后,加一层处理,将数据转换成指令并将结果读取并打印出来:
string ExcuteCommand(const string &cmd)
{
    cout << "get a request cmd: " << cmd << endl;

    FILE *fp = popen(cmd.c_str(), "r");
    if(nullptr == fp)
    {
      perror("popen");
      return "error";
    }
    string ret;
    char buffer;
    while(true)
    {
      char *tmp = fgets(buffer, sizeof(buffer), fp);
      if(tmp == nullptr) break;
      ret += buffer;
    }
    pclose(fp);

    return ret;
}
https://i-blog.csdnimg.cn/direct/646f920e05274a2d8c7563e32c8c869f.png
当然用Windows也是可以的:
https://i-blog.csdnimg.cn/direct/844c0e32f9774c119d8c58dbdcd3f1ec.png
实在我们的xshell原理就类似:
https://i-blog.csdnimg.cn/direct/75fd692e246541a3814a961ce61d3354.png
我们是客户端访问远端服务器后输入数据被剖析成指令,服务器再将结果返回给我们。
9.实现tcp_socket

系列函数剖析:
listen 函数在网络编程中用于将一个套接字从“自动”状态切换到“监听”状态,,通常在调用 socket 函数创建套接字并用 bind 绑定到地点和端口之后使用。
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);



[*]backlog:指定套接字的最大连接队列长度,即在服务器处理新连接之前,内核中允许排队的未决连接数。这个数值决定了在连接哀求被接受之前,内核可以缓存多少个连接哀求。
accept 函数是网络编程中的一个紧张函数,用于从已经处于监听状态的套接字中接受一个连接哀求,并返回一个新的套接字形貌符,该形貌符用于与客户端举行通讯
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);


[*]乐成时,返回一个新的套接字形貌符,该形貌符用于与客户端举行通讯。
[*]三次握手完成后, 服务器调用accept()接受连接;
[*]如果服务器调用accept()时还没有客户端的连接哀求,就阻塞等待直到有客户端连接上来;
[*]addr是一个传出参数,accept()返回时传出客户端的地点和端口号;
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);
connect 函数用于将一个套接字连接到一个长途主机上的指定地点。用于客户端连接服务器,连接乐成后,客户端可以通过该套接字与服务器举行数据互换。
tcp通讯也是全双工的,因为他的发送缓冲区和吸收缓冲区是分开的,所以吸收和发送是可以同时举行的。
我们先来实现简单的tcpsocket的cs通讯:
服务器代码逻辑:
#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
//#include "Task.hpp"
#include "Daemon.hpp"
using namespace std;


const string defaultip = "0.0.0.0";
const int backlog = 5;
extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port, const string &ip = defaultip)
      :_port(port), _ip(ip)
    {
    }

    void InitServer()
    {
      _listensock = socket(AF_INET, SOCK_STREAM, 0);

      struct sockaddr_in _obj;
      bzero(&_obj, sizeof(_obj));
      _obj.sin_family = AF_INET;
      _obj.sin_port = htons(_port);
      _obj.sin_addr.s_addr = INADDR_ANY;

      socklen_t len = sizeof(_obj);
      if (bind(_listensock, (struct sockaddr *)&_obj, len) < 0)
      {
            lg(Fatal, "Server bind is error %s", strerror(errno));
            exit(BindError);
      }

      lg(Info, "Server bind is succes , socketfd is %d ,port :%d", _listensock, _port);

      // 监听
      if (listen(_listensock, backlog) < 0)
      {
            lg(Fatal, "Server listen is error %s", strerror(errno));
            exit(ListenError);
      }
      lg(Info, "Server listen is succes ");
      
    }
    void _test(int _sockefd, uint16_t &port, string &clientip)
    {
      char buffer;
      while (true)
      {
            ssize_t n = read(_sockefd, buffer, sizeof(buffer) - 1); // 留一个位置给 '\0'
            if (n > 0)
            {
                buffer = 0; // 添加终止符,防止溢出
                cout << "client say# " << buffer << endl;
                string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                write(_sockefd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                lg(Info, "%s:%d quit, server close _sockefd: %d", clientip.c_str(), port, _sockefd);
                break;
            }
            else
            {
                lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", _sockefd,
                   clientip.c_str(), port);
                break;
            }
      }
    }
    void Start()
    {
      lg(Info, "tcpServer is running....");
      for (;;)
      {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);

            int _sockefd = accept(_listensock, (struct sockaddr *)&client, &len);
            if (_sockefd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
                continue;
            }
            char clientip;
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            uint16_t clientport = ntohs(client.sin_port);

            lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", _sockefd, clientip);
            _test(_sockefd, client.sin_port, _ip);

            close(_sockefd);
      }
    }
    ~TcpServer() {}

private:
    int _listensock;
    uint16_t _port;
    string _ip;
};
客户端代码逻辑:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
using namespace std;
Log lg;

void Usage(const string &proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n"
            << endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(1);
    }
    string serverip = argv;
    uint16_t serverport = stoi(argv);

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

    while (true)
    {
      int sockfd = 0;
      sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0)
      {
            cerr << "socket error" << endl;
            return 1;
      }

      int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
      if (n < 0)
      {
            lg(Fatal, "Client connect is error %s", strerror(errno));
      }

      while (true)
      {
            string message;
            cout << "Please Enter# ";
            getline(cin, message);

            int n = write(sockfd, message.c_str(), message.size());
            if (n < 0)
            {
                cerr << "write error..." << endl;
            }

            char inbuffer;
            n = read(sockfd, inbuffer, sizeof(inbuffer));
            if (n > 0)
            {
                inbuffer = 0;
                cout << inbuffer << endl;
            }
      }
      close(sockfd);
    }

    return 0;
}
运行起来既实现
https://i-blog.csdnimg.cn/direct/8bcf8e82ec934ff79b9faf817c757bbc.png
可是当前的代码是单进程的,如果有两台主机同时向服务器发送数据,服务器只能处理先连接上的,那我们服务器能同时处理多个客户端的数据该怎么办呢?
我们可以将代码逻辑改成多进程版本的:
核心代码:
pid_t id=fork();
if(id==0)
{
    close(_listensock);
    if(fork()>0)exit(0);
    _test(_sockefd, client.sin_port, _ip);
    close(_sockefd);
    exit(0);
}
close(_sockefd);
pid_t w_id=waitpid(id,nullptr,0);
这时就完成了多进程的代码逻辑。
https://i-blog.csdnimg.cn/direct/8bea3b7c540646d68344ef261d1743c6.png
其中:
if(fork()>0)exit(0);
目标是在子进程中创建孙子进程让孙子进程执行_test代码,如许在执行_test的时间,子进程已经被父进程回收,所以可以继续创建子进程完成逻辑,实现多主机可以同时访问服务器的功能。使用多线程的话也是类似的逻辑,但是创建线程的成本比进程低的多,这里就不演示了。
接下来我们编写一个基于线程池处理汉译英功能的服务器。首先直接将我们之前编写的单例模式的线程池拿过来:
#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 10;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
      pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
      pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
      pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
      pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
      return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
      for (const auto &ti : threads_)
      {
            if (ti.tid == tid)
                return ti.name;
      }
      return "None";
    }

public:
    static void *HandlerTask(void *args)
    {
      ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
      std::string name = tp->GetThreadName(pthread_self());
      while (true)
      {
            tp->Lock();

            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t();
      }
    }
    void Start()
    {
      int num = threads_.size();
      for (int i = 0; i < num; i++)
      {
            threads_.name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_.tid), nullptr, HandlerTask, this);
      }
    }
    T Pop()
    {
      T t = tasks_.front();
      tasks_.pop();
      return t;
    }
    void Push(const T &t)
    {
      Lock();
      tasks_.push(t);
      Wakeup();
      Unlock();
    }
    static ThreadPool<T> *GetInstance()
    {
      if (nullptr == tp_) // ???
      {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
      }

      return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
      pthread_mutex_init(&mutex_, nullptr);
      pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
      pthread_mutex_destroy(&mutex_);
      pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
服务器代码逻辑:
#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>

#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"
using namespace std;


const string defaultip = "0.0.0.0";
const int backlog = 6;
extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int fd, const string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port, const string &ip = defaultip)
      :_port(port), _ip(ip)
    {
    }

    void InitServer()
    {
      _listensock = socket(AF_INET, SOCK_STREAM, 0);

      struct sockaddr_in _obj;
      bzero(&_obj, sizeof(_obj));
      _obj.sin_family = AF_INET;
      _obj.sin_port = htons(_port);
      _obj.sin_addr.s_addr = INADDR_ANY;

      socklen_t len = sizeof(_obj);
      if (bind(_listensock, (struct sockaddr *)&_obj, len) < 0)
      {
            lg(Fatal, "Server bind is error %s", strerror(errno));
            exit(BindError);
      }

      lg(Info, "Server bind is succes , socketfd is %d ,port :%d", _listensock, _port);

      // 监听
      if (listen(_listensock, backlog) < 0)
      {
            lg(Fatal, "Server listen is error %s", strerror(errno));
            exit(ListenError);
      }
      lg(Info, "Server listen is succes ");

    void Start()
    {
      ThreadPool<Task>::GetInstance()->Start();
      lg(Info, "tcpServer is running....");
      for (;;)
      {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);

            int _sockefd = accept(_listensock, (struct sockaddr *)&client, &len);
            if (_sockefd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
                continue;
            }
            char clientip;
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            uint16_t clientport = ntohs(client.sin_port);

            lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", _sockefd, clientip);

            Task t(_sockefd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);
      }
    }
    ~TcpServer() {}

private:
    int _listensock;
    uint16_t _port;
    string _ip;
};
客户端代码逻辑:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
using namespace std;
Log lg;

void Usage(const string &proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n"
            << endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(1);
    }
    string serverip = argv;
    uint16_t serverport = stoi(argv);

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

    while (true)
    {
      int sockfd = 0;
      sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0)
      {
            cerr << "socket error" << endl;
            return 1;
      }
      //客户端发起connect的时候,进行自动随机bind
      int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
      if (n < 0)
      {
            lg(Fatal, "Client connect is error %s", strerror(errno));
            return 1;
      }

      while (true)
      {
            string message;
            cout << "Please Enter# ";
            getline(cin, message);

            int n = write(sockfd, message.c_str(), message.size());
            if (n < 0)
            {
                cerr << "write error..." << endl;
                break;
            }
            else if (n == 0) {
                break;
            }

            char inbuffer;
            n = read(sockfd, inbuffer, sizeof(inbuffer));
            if (n > 0)
            {
                inbuffer = 0;
                cout << inbuffer << endl;
            }
            else if(n == 0) {
                break;
            }
      }
      close(sockfd);
    }

    return 0;
}
英译汉搜刮逻辑:
#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
using namespace std;
Log lg;

const string dictname = "./dict.txt";
const string sep = ":";

//yellow:黄色...
static bool Split(string &s, string *part1, string *part2)
{
    auto pos = s.find(sep);
    if(pos == string::npos) return false;
    *part1 = s.substr(0, pos);
    *part2 = s.substr(pos+1);
    return true;
}

class Init
{
public:
    Init()
    {
      ifstream in(dictname);
      if(!in.is_open())
      {
            lg(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
      }
      string line;
      while(getline(in, line))
      {
            string part1, part2;
            Split(line, &part1, &part2);
            dict.insert({part1, part2});
      }
      in.close();
    }
    string translation(const string &key)
    {
      auto iter = dict.find(key);
      if(iter == dict.end()) return "Unknow";
      else return iter->second;
    }
private:
    unordered_map<string, string> dict;
};
使命代码逻辑:
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"

extern Log lg;
Init init;

class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
      : sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
    {
    }
    Task()
    {
    }
    void run()
    {
      char buffer;
      while(1) {
            ssize_t n = read(sockfd_, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer = 0;
                std::cout << "client key# " << buffer << std::endl;
                std::string echo_string = init.translation(buffer);

                n = write(sockfd_, echo_string.c_str(), echo_string.size());
                if(n < 0)
                {
                  lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
                }
            }
            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;

            }
      }
      close(sockfd_);

    }
    ~Task()
    {
    }

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};
https://i-blog.csdnimg.cn/direct/96531987a5124ee8ada2b209e5b061af.png
10.守护进程

我们之前学习过前台进程和后台进程,在下令行中前台进程是必须要一直存在的,能接受键盘信号的就是前台进程。所以说谁拥有键盘文件谁就是前台进程,一个会话可以有多个后台进程但只能有一个前台进程。&符号就是用来将程序设置到后台运行的:
https://i-blog.csdnimg.cn/direct/4316d128cde04aba8d53bb0f519fe51d.png
jobs指令可以查看后台使命:
https://i-blog.csdnimg.cn/direct/388b966c0420495da35a10077ae752cd.png
fg加使命号可以将后台使命提到前台:
https://i-blog.csdnimg.cn/direct/962b97f803cd434e81158bf035cff4e7.png
ctrl Z可将将其暂停并放回后台:
https://i-blog.csdnimg.cn/direct/1b3f1c0c71214f64a7dc75ba3582448b.png
bg加使命号可将后台使命在跑起来:
https://i-blog.csdnimg.cn/direct/cec44c45020745d0bc1c4fb46298ca00.png
https://i-blog.csdnimg.cn/direct/a27305ab5def410cb118c8510e5f0bd1.png
每次登录xshell就会建立一个会话,所以有会话id的概念,TTY为控制终端,一个进程组只有一个进程的pid和pgid相同,组长就是进程组的第一个。
https://i-blog.csdnimg.cn/direct/3d51a9cbe14d49429d213e0666ebbcfb.png
https://i-blog.csdnimg.cn/direct/02db69209c8245c3ad03df455380bbf5.png
我们可以观察到终端是 ?这表明它没有关联到任何终端。
守护进程的父进程 ID(PPID)是 1,这表明它们可能是由 init启动的。守护进程的会话 ID 通常与其进程组 ID 相同,体现它们属于同一会话
函数setsid:
#include <unistd.h>

pid_t setsid(void);

setsid 函数是用于创建新的会话的系统调用。它的重要作用是将当进步程创建为新的会话的向导者,从而实现与当前会话的断开,通常用于守护进程的创建。也就是说守护进程是自成会话的进程,需要留意的是setsid的调用进程不能是进程组组长,所以我们需要fork子进程执行这个函数。守护进程也是孤儿进程
代码逻辑实现:
#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = "")
{
    // 1. 忽略其他异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2. 将自己变成独立的会话
    if (fork() > 0)
      exit(0);
    setsid();

    // 3. 更改当前调用进程的工作目录
    if (!cwd.empty())
      chdir(cwd.c_str());

    // 4. 标准输入,标准输出,标准错误重定向至/dev/null
    int fd = open(nullfile.c_str(), O_RDWR);
    if(fd > 0)
    {
      dup2(fd, 0);
      dup2(fd, 1);
      dup2(fd, 2);
      close(fd);
    }
}
逻辑剖析:


[*]定义了一个常量 nullfile,指定了 /dev/null,任何写入到它的数据都会被抛弃,读取则返回 EOF。
[*]忽略异常信号这些信号处理设置确保了守护进程不会因为这些信号而被不测终止。
[*]fork():创建一个子进程。父进程退出,子进程继续执行。这让子进程脱离原有的会话。
[*]setsid():创建一个新的会话,并将调用进程设置为该会话的会话组长。如许,进程就不再与终端相关联,可以在后台运行。
[*]chdir(cwd.c_str());:如果提供了工作目录路径,则更改进程的工作目录。通常,守护进程会将工作目录更改为根目录。
[*]open(nullfile.c_str(), O_RDWR);:打开 /dev/null 设备文件。dup2(fd, 0);、dup2(fd, 1);、dup2(fd, 2);:将尺度输入(0)、尺度输出(1)、尺度错误(2)重定向到 /dev/null。如许,守护进程的输入输出不会干扰终端,也不会产生任何不必要的输出。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Linux 网络套接字剖析:实现网络通讯