【Linux进阶之路】Socket —— “UDP“ && “TCP“

  • 温馨提示:文章较长(代码较多),请收藏起来慢慢观看。

一、再识网络

1. 端口号

 在上文我们大致理解了,网络传输的基本流程和相关概念,知道ip地址可以标识唯一的一台主机,但是主机获取到信息的最终目的是为了呈现给上层的用户,即我们所熟知的抖音等APP,既然有很多的APP,具体给哪一个APP呢?

  • 说明:
  1. APP,具体指的是运行起来的程序,即一个一个的进程。
  2. 网络通信的本质是进程之间借助网卡(共享资源)进行通信。
  • 概念
  • 端口号:
  1. 指的是用于标记进程或者服务的逻辑地址
  2. 范围为0 到 65535,分有大致三类:
  1. 系统端口:系统端口范围是从 0 到 1023,这些端口通常被一些众所周知的网络服务占用,比如 HTTP(端口 80)、HTTPS(端口 443)、FTP(端口 21)、SSH(端口 22)等。通常需要root权限才能够进行使用。
  2. 注册端口:注册端口范围是从 1024 到 49151,这一范围的端口通常被一些应用程序或服务占用。普通用户也可进行使用
  3. 动态/私有端口:动态/私有端口范围是从 49152 到 65535,也被称为私有端口或暂时端口。这些端口通常被客户端应用程序使用,用于临时通信。

疑问:既然进程的pid能标识唯一的进程,那为什么不直接捡现成的用呢?

答: 进程pid VS 端口号:

  • 从概念上看:
  1. pid:操作系统管理进程使用。
  2. 端口号:网络通信以及为应用程序提供服务。
  3. 两者实现的解耦合的关系。
  • 从使用形式上看:
  1. pid: 进程创建时才拥有。
  2. 端口号:固定一段范围,0 到 65535。
  • 从用法来看:
  1. pid: 一个进程只能有一个pid。
  2. 端口号:一个进程能有多个端口号,为用户提供不同的服务。
  3. 联系:pid和端口号都是只能对应一个进程。且通过端口号可找到进程pid,从而找到进程。

  • 总结:
  1. 通过IP地址标识唯一的一台主机。
  2. 通过端口号标识唯一的一个进程。
  3. 进而我们可以实现网络之间的通信。

拓展:在实际进行通信的过程中,一般是由客户端访问服务器,由服务器再提供对应的服务。

  • 说明:
  1. 客户端要想访问服务器,首先得知道服务器的ip地址和对应服务的端口号。这些工作早已经由开发人员做好,因此无需担心。
  2. 服务器的ip地址和端口号一般是不能发生变化的,否则客户端就无法访问。因为客户端的载入的服务器的端口号和ip一般是固定的。
  3. 客户端的端口号是动态变化的。这是因为多个app的开发厂商并不互通,因此可能存在端口号冲突的现象,因此要动态绑定端口号,而且这样做更加灵活,安全,高效。
  4. 服务器要对大量用户提供服务,而且用户的IP地址是随机变化的,这也间接的导致了,服务器要在"客户端做一些手脚", 即固定服务器的ip地址和端口号。

2. 网络字节序列

 关于数据用大端还是用小端,就跟鸡蛋先吃大头还是先吃小头一样,没有实际的争论意义,因此我们看到电脑既有大端机,也有小端机。

在这里插入图片描述

  • 说明:big - endian 为大端机的数据,little - endian为小端机的数据。
  • 速记:大同小异反着记——大 “异” 小 “同”。
  • 但是网络里面传输数据,不可能即传输大端数据也传输小端,因此规定统一在网络里面传输大端数据,到对应的主机里面再进行统一的转换,大端不用变,小端再转换一下即可。

  • 相关的接口:

#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(host) to n(net) l(long),即将long类型的数据从主机转网络。其余类似。

3.TCP 与 UDP

传输方式:

  • TCP:面向字节流。
  • UDP:面向数据报。

这是最本质的差别,下面我们进行分析:

  1. 将数据视为连续的字节流进行传输和处理。发送方将数据拆分为字节流并逐个发送,接收方按照接收到的顺序重新组装数据。
  2. 提供了可靠的传输,保证数据按顺序、无差错地传输。它使用基于确认和重传的机制来确保数据的可靠性。
  3. 基于连接的通信方式,需要在发送方和接收方之间建立一个持久的连接。连接的建立和维护需要一定的开销,但可以确保数据的有序传输。
  4. 适用于需要可靠传输和有序性的应用,如文件传输、视频流传输等。

总结:TCP协议,面向字节流,可靠,有连接。适合文件和视频等信息的传输。

  1. 将数据划分为独立的数据,即数据报,每个数据报都携带了完整的信息,可以独立地发送和接收。
  2. 不保证数据的可靠性,每个数据报都是独立传输的,可能会发生丢失、重复或乱序。
  3. 无连接的通信方式,每个数据报都是独立的,不需要事先建立连接。
  4. 对实时性要求较高的应用,如实时音频、视频通信等,因为它可以提供更低的延迟。

总结:UDP协议,面向数据报,不可靠,无连接。适用于对实时性要求高的应用。

  • 说明:这里的可靠和不可靠是一个中性词。不可靠意味着较低的成本,实现更加简单,可靠意味着实现需要较大的代价。因此没有谁好谁坏。

下面我们实现是更为简单的UDP套接字。

  • 在开始之前我们先来解决一个前置问题,主要是服务器的端口问题,一般默认有些端口是禁掉的,不能用于网络之间的通信,因此我们需要开放一些端口供我们之间通信使用。
  • 实现步骤:
  1. 登录所在云服务的官网。(我的是阿里云的)
  2. 点击控制台。
  3. 点击云服务器ESC/轻量级服务器/云服务器,找到对应的云服务器。(我的是轻量级云服务器)
  4. 如果是云服务器ESC/服务器就找到安全组,点击安全组ID进行编辑即可。如果是轻量级服务器就在服务器一栏找到实例id点击,再点击防火墙进行编辑即可。
  • 具体步骤——阿里云轻量级云服务器
    • 第一步:
      在这里插入图片描述
    • 第二步:
      在这里插入图片描述
    • 第三步:
      在这里插入图片描述
    • 第四步:
      在这里插入图片描述

二、套接字

1.sockaddr结构

  • 这是一层抽象化的结构,设计之初是为了统一网络套接字的接口使用,是一套通用的网络套接字,而对应的具体的套接字有 网络套接字 与 域间套接字

图解:
在这里插入图片描述

  • 类似多态的思想,即从抽象到具体。在使用过程中我们可以通过传入通用的套接字类型,并且指定对应的套接字大小,从而说明其对应的具体类型,也就是我们说的多态。

  • 我们实现的是网络编程,使用的是:struct sockaddr_in

  • 具体结构:
    在这里插入图片描述
  1. sin_family_t sin_family; 所属家族协议类型,一般设置为AF_INT/PF_INT,即ipv4类型的协议。
  2. in_port_t sin_port; 端口号。
  3. struct in_addr sin_addr; ip地址。
  • 注意:端口号和ip地址的数据都为网络序列。

2.UDP

  • Log.hpp(记录日志信息)
#pragma once
#include<map>
#include<iostream>
#include<cstdio>
#include<stdarg.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<time.h>
using namespace std;
#define SIZE (4096)

//事件的等级
#define EMRGE 1
#define ALERK 2
#define CRIT  3
#define ERRO  4
#define WARNNING 5
#define NOTICE   6
#define INFORE   7
#define DEBUG    8
#define NONE     9

//输出方向
#define DEFAULTFILE 1
#define CLASSFILE 2
#define SCREAN 0
//说明:一般我们在传参时一般都是以宏的方式进行传参的,
//如果需要打印出字符串可以用KV类型进行映射转换。
map<int,string> Mode = {
   
   
    {
   
   1,"EMERG"},{
   
   2,"ALERK"},{
   
   3,"CRIT"},
    {
   
   4,"ERRO"},{
   
   5,"WARNING"},{
   
   6,"NOTICE"},
    {
   
   7,"INFOR"},{
   
   8,"DEBUG"},{
   
   9,"NONE"}
};

//分类文件处理的后缀。
map<int,string> file = {
   
   
    {
   
   1,"emerg"},{
   
   2,"alerk"},{
   
   3,"crit"},
    {
   
   4,"erro"},{
   
   5,"warning"},{
   
   6,"notice"},
    {
   
   7,"infor"},{
   
   8,"debug"},{
   
   9,"none"}
};
class Log
{
   
   
public:
    void operator()(int level,const char* format,...)
    {
   
   
        //将输入的字符串信息进行输出。
        va_list arg;
        va_start(arg,format);
        char buf[SIZE];
        vsnprintf(buf,SIZE,format,arg);
        va_end(arg);

        //获取时间
        time_t date = time(NULL);
        struct tm* t = localtime((const time_t *)&date);
        char cur_time[SIZE] = {
   
   0};
        snprintf(cur_time,SIZE,"[%d-%d-%d %d:%d:%d]",\
        t->tm_year + 1900,t->tm_mon + 1,
        t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);

        //输入再进行合并
        string Log = "[" + Mode[level] + "]" + \
        cur_time + string(buf) + "\n";
        
        //处理输出方向
        PrintClassFile(level,where,Log);
    }
    void PrintDefaultFILE(string& file_name,const string& mes)
    {
   
   
        int fd = open(file_name.c_str(),O_CREAT | O_WRONLY \
        | O_APPEND,0666);
        write(fd,mes.c_str(),mes.size());
        close(fd);
    }
    //将文件进行分类进行输出。
    void PrintClassFile(int level,int where,const string& mes)
    {
   
   
        if(where == SCREAN)
            cout << mes;
        else
        {
   
   
            string file_name = "./log.txt";
            if(where == CLASSFILE)
                file_name += ("." + file[level]);
            PrintDefaultFILE(file_name,mes);
        }
    }
    void ReDirect(int wh)
    {
   
   
        where = wh;
    }
private:
    int where = SCREAN;
};

说明:在【Linux进阶之路】进程间通信有所提及,具体这个小组件是用来帮助我们显示出日志的时间,等级,出错内容等信息。

1.server端

  • 基本框架:
//所用容器
#include<string>
#include<unordered_map>

//与内存相关的头文件
#include<string.h>
#include<strings.h>

//网络相关的头文件
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h> 

//包装器
#include<functional>

//日志头文件
#include "Log.hpp"

//枚举常量,用于失败退出进程的退出码
enum
{
   
   
    SOCKET_CREAT_FAIL = 1,
    SOCKET_BIND_FAIL,
};
class UdpServer
{
   
   
public:
    UdpServer(uint16_t port,string ip)
    :_port(port),_ip(ip),_sockfd(0)
    {
   
   }
    ~UdpServer()
    {
   
   }
    void Init()
    {
   
   }
    void Run()
    {
   
   }
private:
    int _sockfd;
    string _ip;
    uint16_t _port;
};

1.1 构造函数
  1. 一般我们使用1024以上的端口号即可,此处我们默认使用8080端口。
  2. 云服务器,禁止直接绑定公网ip。
  • . 解决方法——因此我们绑定的时候使用0.0.0.0即任意ip地址绑定即可,即接收所有云服务器地址的发来的信息。
  • . 方法优点——服务可以在服务器上的所有IP地址和网络接口上进行监听,从而提供更广泛的访问范围。
  • 因此在构造函数里,我们给出两个缺省值即可。
//全局定义:
uint16_t default_port = 8080;
string default_string = "0.0.0.0";

//类内
UdpServer(uint16_t port = default_port,string ip = default_string)
:_port(port),_ip(ip),_sockfd(0)
{
   
   }
1.2 Init
  1. 创建套接字
  • 接口
//头文件:
#include <sys/types.h>
#include <sys/socket.h>
//函数声明:
int socket(int domain, int type, int protocol);
/*
参数:
	1:指定通信域,使用AF_INT即可,即IPV4的ip地址。
	2: SOCKET_DGRAM,即使用的套接字类型,指的是UDP类型的套接字。
	3: 指定协议,一般设为0,根据前两个参数系统会自动选择合适的协议。
返回值:
	1.成功返回对应的文件描述符,网络对应的是网卡文件。
	2.失败返回-1。

*/
  1. 绑定套接字
  • 接口:
//头文件:
#include <sys/types.h>
#include <sys/socket.h>
//函数声明:
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

/*
参数:
	1.网络的文件描述符。
	2.sockaddr具体对象对应的地址,为输入型参数。
	3.具体对象对应的大小,为输入型参数。
说明:在传参之前,sockaddr对象应初始化完成。

返回值:
	1.成功返回 0。
	2.失败返回 -1,并设置合适的错误码。
*/
  • 说明:在传入sockaddr具体对象对应的地址时,需要再强转为sockaddr*类型的,因为也传进去了具体对象的大小,所以内部会再识别出具体的对象,再进行处理。
  • 这里的IP地址的形式为字符串类型的,便于用户进行识别,而在网络当中是usiged int 类型的,中间需要转换一下。

  • 实现代码:

#include<string>
#include<iostream>
using std::string;
using std::cout;
using std::endl;

struct StrToIp
{
   
   
	unsigned int str_to_ip(const string& str)
	{
   
   
		int ssz = str.size();
		int begin = 0;
		int index = 3;
		for (int i = 0; i <= ssz; i++)
		{
   
   
			if (str[i] == '.' || i == ssz)
			{
   
   
				string tmp = str.substr(begin,i);
				begin = i + 1;
				unsigned char n = stoi(tmp);
				if (index < 0) return 0;
				part[index--] = n;
			}
		}
		//auto p = (unsigned char*)&ip;
		//for (int i = 0; i < 4; i++)
		//{
   
   
		//	*(p + i) = part[i];
		//}
		//return ip;

		return *((unsigned int*)part);
	}
	unsigned char part[4] = {
   
    0 };

	//unsigned int ip = 0;

};

int main()
{
   
   
	StrToIp s;
	cout << s.str_to_ip("59.110.171.160") << endl;
	return 0;
}
  1. 我们将字符串分为四部分,然后转换为char类型的四个变量,存储即可。
  2. 这四个部分我们存放在数组或者单独存都可以,这里我采用数组便于操作。
  3. 如果为数组,具体转换为int变量时,应注意四个部分的存储顺序。
  • 运行结果:

在这里插入图片描述

  • 说明:
  1. 我所在的机器为小端机,数据是低位放在低地址处,所以应该倒着存每一段。
  2. 如果为大端机,数据是高位放在低地址处,所以应该正着存每一段。
  3. 最后强转取数据即可。
  • 补充: 指针指向的是对象的低地址处。

在实际编程的过程中,相应的接口已经准备好,不需要手动的写,但相应的原理还是要清楚的。

  • 字符串转地址的网络序列接口:
//头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);

/*
参数:
	1.转化的ip地址的字符串。
	2.输出型参数,in_addr的变量。
返回值:
	1.成功返回非零值,通常为1.
	2.失败返回零值。
*/
in_addr_t inet_addr(const char *cp);
/*
参数:
	1.转化的ip地址的字符串。
返回值:
	1.成功返回对应的ip值。
	2.失败返回INADDR_NONE,其定义为 (in_addr_t) -1。
*/

  • 主机ip地址转字符串的接口:
//头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);

/*

参数:存放主机序列的ip地址
返回值:字符串形式的ip。

*/

  • 实现代码:
    void Init()
    {
   
   
        //1.创建套接字,即创建文件描述符
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
   
   
            lg(CRIT,"socket fail,error message is %s,error \
            number is %d ",strerror(errno),errno);
            exit(SOCKET_CREAT_FAIL);
        }
        lg(INFORE,"socket fd is %d,socket success!",_sockfd);
        //2.绑定套接字
        /*
            注意:主机序列都要转成网络序列。
        */
        
        // 2.1初始化域间套接字
        struct sockaddr_in server_mes;
        bzero(&server_mes,sizeof(server_mes));
        server_mes.sin_family = AF_INET;
        server_mes.sin_port = htons(_port); 
        server_mes.sin_addr.s_addr = inet_addr(_ip.c_str());
        socklen_t len = sizeof(server_mes);
        
        // server_mes.sin_addr.s_addr = INADDR_ANY;; 
        
        //任意地址转网络序列
        
        // 2.2 绑定域间套接字
        int ret = bind(_sockfd,(const sockaddr*)&server_mes,len);
        if(ret < 0)
        {
   
   
            lg(CRIT,"bind fail,error message is %s,\
            error number is %d ",strerror(errno),errno);
            exit(SOCKET_BIND_FAIL);
        }
        lg(INFORE,"ret is %d,bind success!",ret);

    }

1.3 Run
  1. 等待客户发信息

接口

//头文件
#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);

/*
参数:
	1.文件描述符。
	2.缓存区,读取用户发来的信息。
	3.缓存区的大小。
	4.一般使用默认值0即可。
	5.src_addr变量的地址,用于接收用户的网络信息,输出型参数。
	6.addrlen用于接受用户的src_addr具体对象的长度,输入输出型参数。

返回值:
	1.成功,返回接受的字节个数。
	2.连接关闭,返回0。
	3.错误返回-1.设置合适的错误码。
*/

  1. 给客户提供服务

接口:

//头文件:
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen)	
参数:
	1.文件描述符。
	2.缓存区,存放发给用户的信息。
	3.缓存区的大小。
	4.一般使用默认值0即可。
	5.src_addr变量的地址,用于存放用户的网络信息。
	6.addrlen用于存放用户的src_addr具体对象的长度。

返回值:
	1.成功,返回实际发送的字节个数。
	3.错误返回-1.设置合适的错误码。
  • 实现代码:
 void Run()
 {
   
   
     for(;;)
     {
   
   
     	 //存放用户消息的缓存区
         char buffer[1024] = {
   
   0};
         //用于存放用户的网络信息
         struct sockaddr_in client_mes; 
         socklen_t len;
         //收消息
         ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1\
         ,0,(sockaddr*)&client_mes,&len);
         if(n < 0)
         {
   
   
             lg(WARNNING,"recvfrom message fail,waring \
             message is %s",strerror(errno));
             continue;<
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值