Linux网络编程 -- 网络套接字预备与udp

embedded/2024/10/17 10:37:25/

本文主要介绍网络编程的相关知识,在正式介绍网络编程之前,我们得先了解一些前置的知识。

1、端口号

我们上网其实就是两种动作,一个是将远处的数据拉取到本地,另一个是把我们的数据发送给远端。其实大部分的网络通信行为都是用户触发的,而在计算机中,谁表示用户呢?答案是进程。当用户接受到数据后,OS要将对应的数据给特定的服务进程。一台机器上有许多的服务进程,而我们用特定的端口号(port)来标识这个服务进程。

所以当我们将IP与端口号进行绑定,就可以得出互联网中的唯一一个进程了。这种方式也叫做套接字通信,服务器端和客户端的通信一般都采用这种方式。

为什么我们要用端口号来标识一个进程呢?不是有进程的pid吗?首先是因为进程的pid是不断变化的,其次如果使用pid的话,网络服务就和操作系统进行强关联了。此时如果我们修改一下操作系统对进程的标识方式,那么对上层的网络服务也需要进行大幅修改。另外OS中并不是每个进程都有端口号的,一般只有网络进程才有端口号。端口号和进程pid在OS会通过特定数据结构关联起来,例如哈希表。

2、TCP和UDP协议

在正式开始介绍网络编程之前,我们先简单了解一下UDP和TCP协议(后面具体介绍)

TCP(传输控制协议)和UDP(用户数据报协议)是互联网上用于数据传输的两种重要协议,它们的主要区别在于以下几个方面:

  1. 连接性

    • TCP 是面向连接的协议,意味着在数据传输之前,需要建立一个连接,在传输完成后需要断开连接。
    • UDP 是无连接的,它发送数据之前不需要建立连接,数据可以直接发送给接收方。
  2. 可靠性

    • TCP 提供了可靠的服务。它确保数据包的顺序传输,并且通过确认和重传机制保证数据的完整性。
    • UDP 不保证数据的可靠传输,它只负责发送数据,不保证数据包的顺序或是否到达。
  3. 速度与效率

    • TCP 由于其可靠性机制,速度相对较慢,因为它需要时间来建立连接、确认数据包和进行重传。
    • UDP 由于没有这些机制,通常更快,适用于对实时性要求较高的应用,如视频会议和在线游戏。
  4. 数据流控制

    • TCP 有流量控制和拥塞控制机制,可以根据网络状况调整数据传输的速度。
    • UDP 没有这样的控制机制,发送速率不会因网络状况而改变。
  5. 用途

    • TCP 常用于要求高可靠性的应用,如网页浏览、电子邮件和文件传输。
    • UDP 常用于实时应用,如流媒体、VoIP(网络电话)和在线游戏,这些应用对速度的要求高于数据完整性。
  6. 头部开销

    • TCP 头部较大,因为它需要包含更多的信息来保证数据的可靠传输。
    • UDP 头部较小,处理起来更快,开销更小

这里只是简单介绍,看不懂没有关系,有个基础概念即可。

 3、网络字节序

在我们机器中,分为大端机和小端机,当我们在网络进行传输时,可能会面临大端机向小端机传输数据的情况。此时接受方读取数据时就可能会出现异常,所以这里统一将网络的数据定为大端。但是每次都要我们手动对数据进行大小端的转换未免太过麻烦,所以系统为我们提供特定的接口统一转换,后续会对其进行介绍。

4、接口预备知识

socket编程,是有很多的种类,有的是专门用来本地通信的(Unix socket),有的是用来跨网络进行通信的(inet socket),有的是用来进行网络管理的(raw socket )。这些套接字类型非常多,为了减少学习的成本,linux编写者就决定让这些套接字使用统一的接口。而OS是由C语言进行编写的,涉及到统一类型的问题就必须和结构体相关联。其中有关套接字的结构体常见有三种。

 我们常用的类型是struct sockaddr,但实际上我们用于存储数据的结构体是struct sockadd_in和struct sockaddr_un。与网络编程相关的接口都使用的是sockaddr结构体,所以我们需要先用sockaddr_in结构体存储数据,然后强转成sockaddr结构。这种特性和C++多态非常类似。这里我们着重关注sockaddr结构即可,当我们将sockaddr_in 结构强转成sockaddr后,相关的接口依然能够识别原来sockaddr_in结构体内的数据。(sockaddr_un同理)

5、相关接口的介绍与认识

1、socket

该函数用于创建套接字,第一个参数用于指定套接字的域,第二个参数是套接字的类型,第三个参数在前两个参数确定的情况下填零即可。该接口成功调用的返回值是一个文件描述符,失败就返回-1,网络套接字创建以后相当于绑定了该文件描述符。这里我们一般就使用AF_INET。

第一个参数列表

第二个参数列表(使用udp协议时,我们就需要将该参数设置成SOCK_DGRAM)

2、bind

在创建完套接字后,我们必须要将创建的套接字与端口号和ip地址进行绑定,也就是将网络服务与本地的文件描述符绑定(先这样理解)。所以这里我们需要引入一个接口bind

第一个参数表示创建套接字的文件描述符,第二个是网络套接字的相关结构体(就是我们上文所提到的sockaddr,不过实际上我们使用的是sockaddr_in),第三个参数表示第二个参数代表的结构体大小。

在使用struct sockaddr_in之前,我们首先要将其定义出来,初始化后再对各个成员进行初始化。

sin_family 我们一般初始化成AF_INET即可,而sin_port和sin_addr的初始化需要特别注意的是,这两个成员再初始化之前,我们都需要对其作主机序列转成网络序列的操作,也就是转成大端,除此之外,由于IP地址是点分十进制风格的字符串来表示的,所以在网络传输前,还要将其变成4字节的IP。在初始化IP地址时,我们发现sockaddr_in中,表示IP地址的成员是一个结构体,而这个结构体内只有一个成员,在初始化的时候需要注意以下。上述这些操作都不需要我们手动的实现,OS已经提供了相关的接口。

端口的主机序列转网络序列的相关接口(这里我们使用第二个接口,因为我们一般将端口设成16位)

当前IP的主机序列转网络序列并将其转成4字节IP的相关接口(这里我们一般使用第二个接口)

3、recvfrom

该接口用于接收网络中数据,第一个参数是服务端或客户端绑定的文件描述符,第二个参数是一个缓冲区的指针,用于存放接收的数据,第三个参数用于表示接收数据的长度,第四个参数是位掩码,用于控制该函数,我们通常填零即可(因情况而定),第五个参数也是一个输出型的参数,用于接收发送方的信息,第六个参数用于表示第五个参数的大小。返回值表示实际读取到的数据长度。

4、sendto

该接口用于向特定主机发送数据,第一个参数服务端或客户端绑定的文件描述符,第二个参数是一个缓冲区的指针,表示要发送的数据。第三个参数表示缓冲区的大小,第四个参数表示位掩码,表示对该函数的控制,一般设置为零即可,第五个参数表示目标主机的相关信息,第六个参数表示第五个参数的大小。返回值表示发送出去的数据长度。

示例代码(udp)

Main.cc(服务端)

#include <iostream>
#include <memory>
#include "Udpserver.hpp"void Usage()
{std::cout << "./Main.cc  server_port\n"<< std::endl;
}
int main(int argc, char* argv[])
{if(argc != 2){Usage();return 0;} EnableScreen();//std::string server_ip = argv[1];int server_port = std::stoi(argv[1]);std::unique_ptr<Udpserver> ptr = std::make_unique<Udpserver>(server_port);ptr->InitServer();ptr->Start();return 0;
}

Udpserver.hpp

#pragma once
#include <iostream>
#include "Log.hpp"
#include <strings.h>
#include "Inetaddr.hpp"
#include <cstring>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum
{SOCKET = 1,BIND
};class Udpserver
{
public:Udpserver(uint16_t port) : _port(port), _isrunning(false){}void InitServer(){_fd = socket(AF_INET, SOCK_DGRAM, 0);if (_fd < 0){LOG(INFO, "socket fail")exit(SOCKET);}struct sockaddr_in infor;bzero(&infor, sizeof(infor));//清空数据infor.sin_family = AF_INET;// 主机序列转网络序列infor.sin_port = htons(_port);infor.sin_addr.s_addr = htonl(INADDR_ANY);// 绑定socklen_t len = (socklen_t)sizeof(infor);int count = bind(_fd, (struct sockaddr *)&infor, len);if (count < 0){LOG(ERROR, "bind fail ...")exit(BIND);}LOG(INFO, "bind success")}void Start(){_isrunning = true;LOG(INFO,"begin server...")while (1){char buffer[1024];memset(buffer, 0, sizeof(buffer));struct sockaddr_in src;socklen_t len = (socklen_t)sizeof(src);ssize_t rnum = recvfrom(_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&src, &len);if (rnum > 0){buffer[1023] = 0;Inetaddr addr(&src);LOG(INFO, "receive informaiton success")printf("#[%s:%d]: %s\n",addr.IP().c_str(),addr.Port(),buffer);ssize_t snum = sendto(_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&src, len);}}_isrunning = false;}~Udpserver(){}
private:int _fd;bool _isrunning;uint16_t _port;//std::string _IP;
};

client.cc(客户端)

#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include "Log.hpp"
#include <strings.h>
#include <arpa/inet.h>
#include <cstring>
#include <netinet/in.h>
using namespace std;
void Usage()
{std::cout << "./Main.cc server_ip  server_port\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage();return 0;}// 创建套接字int fd = socket(AF_INET, SOCK_DGRAM, 0);if (fd < 0){LOG(FATAL, "socket fail...")exit(-1);}struct sockaddr_in client;bzero(&client, sizeof(client));client.sin_family = AF_INET;client.sin_port = htons(stoi(argv[2]));client.sin_addr.s_addr = inet_addr(argv[1]);// client 不需要显示地绑定客户端。OS会在client发送数据时,随机绑定一个端口号// 通信std::string message;while (1){std::cout << "Please Enter: ";std::getline(std::cin, message);sendto(fd, message.c_str(), message.size(), 0, (struct sockaddr *)&client, sizeof(client));struct sockaddr_in peer;socklen_t len = sizeof(peer);char buffer[1024];memset(buffer, 0 , sizeof(buffer));ssize_t n = recvfrom(fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){std::cout << buffer << std::endl;}}return 0;
}

Ineraddr.hpp

#include<iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>class Inetaddr
{
private:void Init(){port = ntohs(_src->sin_port);ip = inet_ntoa(_src->sin_addr);}
public:Inetaddr(struct sockaddr_in* src):_src(src){Init();}std::string IP(){return ip;}uint16_t Port(){return port;}~Inetaddr(){}
private:struct sockaddr_in* _src;std::string ip;uint16_t port;
};

Log.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>
#include <pthread.h>
#include <fstream>
enum Level
{INFO = 0,DEBUG,WARNING,ERROR,FATAL};
std::string Level_tostring(int level)
{switch (level){case INFO:return "INFO";case DEBUG:return "DEBUG";case WARNING:return "ERROR";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "Unkown";}
}pthread_mutex_t _glock = PTHREAD_MUTEX_INITIALIZER;
bool _is_save = false;
const std::string filename = "log.txt";void SaveLog(const std::string context)
{std::ofstream infile;infile.open(filename,std::ios::app);if(!infile.is_open()){std::cout << "open file failed" << std::endl;}else{infile << context;}infile.close();
}
std::string Gettime()
{time_t cur_time = time(NULL);struct tm *time_data = localtime(&cur_time);if (time_data == nullptr){return "None";}char buffer[1024];snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d",time_data->tm_year + 1900,time_data->tm_mon + 1,time_data->tm_mday,time_data->tm_hour,time_data->tm_min,time_data->tm_sec);return buffer;
}
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{std::string levelstr = Level_tostring(level);std::string time = Gettime();// 可变参数char buffer[1024];va_list args;va_start(args, format);vsnprintf(buffer, sizeof(buffer), format, args);va_end(args);std::string context = "[" + levelstr + "]" + "[" + time + "]" + "[" + "line : " + std::to_string(line) + "]" + "[" + filename + "]" + ": " + buffer;pthread_mutex_lock(&_glock);if(!issave){std::cout << context << std::endl;}else{SaveLog(context);}pthread_mutex_unlock(&_glock);
}#define LOG(level, format, ...)                                          \do                                                                   \{                                                                    \LogMessage(__FILE__, __LINE__, _is_save, level, format, ##__VA_ARGS__); \} while (0);
#define EnableFile()    \do                  \{                   \_is_save = true; \} while (0);
#define EnableScreen()   \do                   \{                    \_is_save = false;\} while (0);

代码编写中及测试过程中的注意事项

<1>在客户端中,我们是不需要显示地绑定一个端口号的,操作系统会第一次连接时自动帮我们绑定一个随机端口号。如果帮显示绑定一个端口号,那么可能就会造成以下情况,当你的主机上需要同时启动两款App,但是这两款App绑定了同一个端口号,此时就会造成一个App启动后,另一个个App启动失败。

<2>当我们接收到sockadd_in结构体后,如果需要打印结构体内的信息,需要对对其进行从网络序列转成主机序列的操作,这也就是Inetaddr文件存在的原因(这里我封装了)。

<3>服务端不推荐绑定固定的ip(我们一般就绑定为0,表示能够处理任何ip发送的服务)。在云服务器上也不允许绑定公网的ip,如果需要在云服务器上绑定ip,则需要在对应的云服务的安全组上添加对应的端口。

以上就是所有内容


http://www.ppmy.cn/embedded/127206.html

相关文章

【多模态论文阅读系列二】— MiniCPM-V

校招/实习简历修改、模拟面试欢迎私信《MiniCPM-V: A GPT-4V Level MLLM on Your Phone》 在本节中&#xff0c;我们介绍了MiniCPM-V的模型架构&#xff0c;概述了其总体结构和自适应高分辨率视觉编码方法。MiniCPM-V系列的设计理念是在性能和效率之间实现良好的平衡&#xff0…

2024.10.12 java笔试面试

1.工厂模式 工厂模式是一种创建型设计模式&#xff0c;旨在通过将对象的创建过程与其使用分离。 简单工厂模式&#xff1a;又叫静态工厂方法&#xff0c;由一个工厂类根据参数决定创建哪种具体类的实例 工厂方法模式&#xff1a;定义一个用于创建对象的接口&#xff0c;将实…

大数据笔记之 Hadoop 常用 Shell 命令(一)

文章目录 前言一、HDFS上的操作1.1 查看文件或文件夹1.2 修改文件或目录权限1.3 创建或删除文件夹1.4 移动或创建文件1.5 信息统计1.6 设置 hdfs 中文件的副本数量 二、在本地上传或下载的操作2.1 从本地上传文件到HDFS2.2 从HDFS下载文件到本地 总结 前言 前无言 一、HDFS上的…

mysql数据备份

为什么写这个话题&#xff0c;原因很简单&#xff0c;在实现业务逻辑的时候很多更新操作没有校验where后面的条件&#xff0c;导致整个表的数据被更新了&#xff0c;工作这么多年了&#xff0c;从入行到现在还在不时的发生&#xff0c;当然避免的方式有很多&#xff0c;但是难免…

RK3588 buildroot 制作的系统增加 docker 支持

RK3588 buildroot 制作的系统增加 docker 支持 简介 使用 ubuntu 系统使用一个指令就可以安装 docker ,因为工作需要,系统不能使用 ubuntu 而只能使用buildroot 制作系统,下面我们就一起看下如果在 buildroot 系统上安装 docker 安装 docker 前的内核配置 运行 docker 需要…

【设计模式】装饰者模式

装饰者模式 角色和buff进行解释 步骤 角色和Buff共有一个Component&#xff0c;理解为有同一个操作&#xff0c;给予Buff里面可以填充角色。角色有一个基类&#xff0c;Buff有一个基类&#xff0c;因为有多种Buff 理解 不是常规理解上的给角色填装Buff&#xff0c;角色作为…

axios的使用

在 Vue 项目中&#xff0c;封装 Axios 并实现加密、重复请求优化、请求取消、页面切换时取消未完成的请求、以及区分上传和下载操作是非常常见的需求。下面将逐一讲解这些需求的实现方式。 1. Axios 的基本封装 首先&#xff0c;我们可以将 Axios 封装到一个服务层中&#xf…

鸿蒙--播放器状态控制

各个页面共享同一个播放状态,而且可以互相控制,如果传递来传递去会非常的麻烦,但是他们都是Tabs组件内的,我们在index页面提供一个状态,在各个组件接收即可 创建两个子组件,一个是播放控制的子组件,一个是背景播放的子组件 背景播放组件