网络套接字1

网络套接字1

📟作者主页:慢热的陕西人

🌴专栏链接:Linux

📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言

本博客主要内容讲解了udp的Linux环境下的使用,以及网络的一些基础知识

文章目录

  • 网络套接字1
    • 1.预备知识
      • 1.1理解源IP地址和目的IP地址
      • 1.2认识端口号
      • 1.3理解"端口号"和"进程ID"
      • 1.4理解源端口号和目的端口号
      • 1.5认识TCP协议
      • 1.6认识UDP
      • 1.7网络字节序
    • 2.socket编程接口
      • 2.1socket常见API
      • 2.2sockaddr结构
    • 3.实现一个C/S示例代码
      • 3.1V1
      • 3.2V2
      • 3.3V3
      • 3.4V4版本
      • 3.5提供一个Windows客户端
      • 3.6客户端和服务端的源码:

1.预备知识

1.1理解源IP地址和目的IP地址

唐僧例子1

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址 .

思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析 。

1.2认识端口号

端口号(port)是传输层协议的内容.

  • 端口号是一个2字节16位的整数 ;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理 ;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用;

1.3理解"端口号"和"进程ID"

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这
两者之间是怎样的关系 ?

一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

1.4理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要
发给谁”;

1.5认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题

  • 传输协议层
  • 有连接
  • 可靠传输
  • 面向字节流

1.6认识UDP

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

1.7网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址;
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节 ;
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#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,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

2.socket编程接口

2.1socket常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

2.2sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同

image-20240308215923728

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址 ;
  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容;
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

3.实现一个C/S示例代码

3.1V1

准备工作:网络套接字的创建和对应的端口绑定

        void InitServer(){//1.创建socket接口,打开网络文件//数据报套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);//返回的是一个网络文件的文件描述符if(sock_ < 0){std::cerr << "create socket error" << strerror(errno) << std::endl;exit(SOCKET_ERR); //用枚举构造的一个退出码}std::cout << "create socket success:" << sock_ << std::endl;//2.给服务器指明IP地址和端口号portstruct sockaddr_in local; //这个local,定义在用户空间特定的函数栈帧上,而不是内核里bzero(&local, sizeof(local)); //将local初始化为0//对local进行赋值local.sin_family = AF_INET;  //指定为Address_Familylocal.sin_port = htons(port_); //将主机字节序的port转化为对应的网络字节序// inet_addr: 1,2两个功能都具备// 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化// 2. 需要将主机序列转化成为网络序列// 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IPif(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0)  //绑定端口号{std::cerr << "bind socket error" << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind socket success:"<< sock_ << std::endl;}

v1版本中我们发现我们的服务端在绑定的时候却出错了!这是为什么呢?

云服务器不需要,bind对应的ip地址,需要让服务器自己指定IP地址; 我们自己本地的虚拟机或者物理机是支持的。

[mi@lavm-5wklnbmaja udpv1]$ ./udp_server 
server addr1.1.1.1:8082
create socket success
bind socket errorCannot assign requested address

我们如此去处理:

并且因为INADDR_ANY是一个缺省的零值,我们并不需要对他进行主机转网络化。

// 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,
local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IP

接下来完成收发消息:

收消息的接口:

ssize_t recvfrom(int socket, void *restrict buffer, size_t length,int flags, struct sockaddr *restrict address,socklen_t *restrict address_len);
  1. socket:这是已建立连接的套接字文件描述符,用于接收数据。

  2. buffer:这是指向用于存储接收数据的缓冲区的指针。

  3. length:指定接收数据的最大长度。

  4. flags

    :用于控制接收数据的方式。常见选项包括:

    • MSG_WAITALL:阻塞等待,直到接收到指定长度的数据。
    • MSG_DONTWAIT:非阻塞模式,如果没有数据可读,立即返回-1。
  5. address:这是指向 struct sockaddr 结构的指针,用于存储发送方的地址信息。

  6. address_len:表示 address 结构的字节数。

需要注意的是,recvfrom 函数在 UDP 协议中返回长度为 0 的数据是可行的。因为在 UDP 的情况下,它会形成 20 字节的 IP 首部(IPv4)和一个 8 字节的 UDP 首部,而没有数据的 IP 数据报。因此,UDP 是无连接的协议,不需要真正的发送缓冲区。

发消息的接口:

ssize_t sendto(int socket, const void *message, size_t length,int flags, const struct sockaddr *dest_addr,socklen_t dest_len);

sendto 函数用于将数据从套接字发送给目标主机。

  1. socket:这是已建立连接的套接字文件描述符,用于发送数据。

  2. message:这是指向要发送数据的缓冲区的指针。

  3. length:指定要发送数据的长度。

  4. flags

    :用于控制发送数据的方式。常见选项包括:

    • MSG_DONTWAIT:非阻塞模式,如果无法立即发送数据,立即返回-1。
  5. dest_addr:这是指向 struct sockaddr 结构的指针,用于指定目标主机的地址信息。

  6. dest_len:表示 dest_addr 结构的字节数。

需要注意的是,sendto 函数在 UDP 协议中不需要建立连接,因此不需要经过连接操作。它专门为 UDP 协议提供,因此参数中包含了目标地址信息。

我们完成服务端的收消息和发消息模块:

        void Start(){char buffer[1024]; //我们规定网络通信的时候最大时候是1024字节while(true){//收消息struct sockaddr_in peer;socklen_t len = sizeof(peer); // 这里一定要写清楚,未来缓冲区的大小int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 这里的-1为了兼容C向字符串的结尾加\0if(n > 0) buffer[n] = '\0'; //读取成功,设置\0else continue; //否则的话继续读取//拿到地址和端口,并且完成网络转主机化std::string clientip = inet_ntoa(peer.sin_addr);uint16_t clientport = ntohs(peer.sin_port); std::cout <<clientip << "-"<< clientport << "#get massege : " << buffer << std::endl;//发消息sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, sizeof(peer));}}

我们运行以后,可以用netstat -nuap的指令去查看我们对应的当前在Linux主机上运行的网络服务:

[root@lavm-5wklnbmaja test_db]# netstat -nuap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
udp        0      0 0.0.0.0:68              0.0.0.0:*                           2658/dhclient       
udp        0      0 172.16.0.3:123          0.0.0.0:*                           1845/ntpd           
udp        0      0 127.0.0.1:123           0.0.0.0:*                           1845/ntpd           
udp        0      0 0.0.0.0:123             0.0.0.0:*                           1845/ntpd           
udp        0      0 172.16.0.3:47847        169.254.169.240:53      ESTABLISHED 11484/nscd          
udp        0      0 0.0.0.0:8081            0.0.0.0:*                           29515/./udp_server  
udp6       0      0 fe80::d662:97b7:397:123 :::*                                1845/ntpd           
udp6       0      0 ::1:123                 :::*                                1845/ntpd           
udp6       0      0 :::123                  :::*                                1845/ntpd    

完成对应的客户端:

static void Usage(string proc)
{cout << "Usage:\n\t" << proc << "serverip  serverport  "<< "port\n" << endl;
}//./udp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1];  //获取ipuint16_t serverport = atoi(argv[2]);  //获取portint sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "create socket error " << std::endl;exit(SOCKET_ERR);}//client 这里要不要bind呢?要的!//要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。//为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS//那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的//同一家公司的port号,需要统一和规范的//明确server是谁struct sockaddr_in server;memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0server.sin_family = AF_INET; //指定我们的网络模式为ipv4server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给serverserver.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式while(true){//用户输入消息string message;cout << "Please Enter# ";cin >> message;//什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文//发送给服务端sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));//收char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);if(n > 0) // 收到了数据{buffer[n] = '\0';cout << "server echo# " << buffer << endl;}}return 0;
}

这里我们进行远端测试的时候,要先打开自己服务器上udp的端口防火墙,要不然会测试失败。

3.2V2

我们的v1版本实现的是网络的IO问题,那么我们接下来要实现的是要对我们服务器收到的消息进行业务处理

1.做字符串处理,小写转大写

std::string transactionString(std::string requets)
{std::string result;char c;for(auto& r : requets){if(islower(r)){c = toupper(r);result.push_back(c);}elseresult.push_back(r);}return result;
}

2.做命令处理

在本地把命令发给服务器,让服务器在本地执行,并且把执行结果返回给客户端

这里我们需要用到popen接口:

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
  1. popen(const char *command, const char *type)
    • 参数:
      • command:一个字符串,表示要执行的命令或可执行程序的名称。
      • type:一个字符,指定打开方式。它只能是 'r''w',分别表示读取命令的标准输出或写入命令的标准输入。
    • 功能:
      • popen() 函数创建一个管道,并通过 fork 或者启动一个子进程来执行指定的 command
      • 如果 type'r',则从应用程序的标准输出读取内容并返回一个文件指针。
      • 如果 type'w',则将数据传递给应用程序的标准输入。
      • popen() 的返回值是一个文件指针,成功时非空,失败时为 NULL
  2. pclose(FILE *stream)
    • 参数:
      • stream:先前由 popen() 返回的文件指针。
    • 功能:
      • pclose() 用于关闭由 popen() 创建的管道和文件指针。
      • 成功时,返回子进程的终止状态;失败时,返回 -1,错误原因存储在 errno 中。
bool isPass(const std::string& command)
{   bool pass = true;auto pos = command.find("mv");if(pos != std::string::npos) pass = false;pos = command.find("rm");if(pos != std::string::npos) pass = false;// pos = command.find("");// if(pos != std::string::npos) pass = false;return pass;
}std::string excuteCommend(const std::string& command)
{//1.安全检查if(!isPass(command)) return "Bad command!";//2.业务处理FILE* fp = popen(command.c_str(), "r");if(fp == nullptr) return "None"; //3.获取结果char line[1024];std::string result;while(fgets(line, sizeof(line), fp) != NULL){result += line;}pclose(fp);return result;
}

3.3V3

将我们服务器收到的每一个消息,发送给每一个服务端,模拟一个群聊的功能:

那么过程中有人收消息,有人发消息,类似于一个cp模型:所以我们直接使用之前使用过的一个环形队列的模型;

那么为了维护唤醒队列的线程安全,我们引入之前实现的LockGuard

同时我们引入了Thread.hpp

思路:主要的思路是,每当一个客户端向服务端发送消息的时候,

发的进程:先在map内部去检测,查看当前的用户是否在map内部,如果不在,我们就push到map,然后将消息插入到我们的环形队列中。

收的进程:先从环形队列中取出收到的消息,然后将map中的peer去出放在一个vector中(这个过程要保证线程安全),最后在遍历vector,将消息广播出去,也就是发给曾经给服务器发过消息的客户端。

//RingQueue.hppconst int N = 50;#include<vector>
#include<semaphore.h>using namespace std;template<class T>
class RingQueue
{private://P操作void P(sem_t &m){sem_wait(&m);}//V操作void V(sem_t &m){sem_post(&m);}void lock(pthread_mutex_t &m){pthread_mutex_lock(&m);}void unlock(pthread_mutex_t &m){pthread_mutex_unlock(&m);}
public:RingQueue(int num = N):_cap(num),_v(num) ,_start_step(0), _end_step(0){sem_init(&_data_sem, 0, 0);  //数据初始量为零sem_init(&_space_sem, 0, _cap); //空间初始量为_cappthread_mutex_init(&_con_mutex, nullptr);pthread_mutex_init(&_pro_mutex, nullptr);}void push(const T& in){P(_space_sem);     //p操作之后不需要判断是否有资源,因为p操作通过之后一定有资源lock(_pro_mutex);_v[_end_step++] = in;_end_step %= _cap;unlock(_pro_mutex);V(_data_sem);}void pop(T* out){P(_data_sem);lock(_con_mutex);*out = _v[_start_step++];_start_step %= _cap;unlock(_con_mutex);V(_space_sem);}~RingQueue(){sem_destroy(&_data_sem);sem_destroy(&_space_sem);pthread_mutex_destroy(&_con_mutex);pthread_mutex_destroy(&_pro_mutex);}private:vector<T> _v;  //用于模拟环形队列sem_t _data_sem; //表示数据的信号量sem_t _space_sem; //表示空间的信号量int _cap;                //表示环形队列的大小int _start_step;        //表示消费者的位置int _end_step;          //表示生产者的位置pthread_mutex_t _con_mutex;pthread_mutex_t _pro_mutex;};//LockGuard.hpp#pragma#include<mutex>class Mutex
{
public:Mutex(pthread_mutex_t* pthread) : _pthread(pthread){}void lock(){pthread_mutex_lock(_pthread);}void unlock(){pthread_mutex_unlock(_pthread);}~Mutex(){}
private:pthread_mutex_t * _pthread;};class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex) :_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}private:Mutex _mutex;
};//Thread.hpp#pragma once#include<pthread.h>
#include<unistd.h>
#include<string>class Thread
{public:typedef enum{NEW = 0,RUNING,EXIT}ThreadStatus;// typedef void* (*func_t)(void*);using func_t = std::function<void()>;Thread(int mum, func_t func) :_tid(0), _status(NEW), _func(func){char name[128];snprintf(name, sizeof name, "thread-%d", _tid);_name = name;}int status() {  return _status; }string thread_name(){   return _name; }pthread_t threadid(){if(_status == RUNING){return _tid;}else return 0;}static void* runHelper(void* args){Thread* ts = (Thread*) args; //通过这种方式拿到对象(*ts)();return nullptr;}void operator()(){if(_func != nullptr) _func(); //使用仿函数的方式运行对应的线程进入函数}void run(){int n = pthread_create(&_tid, nullptr, runHelper, this);if(n != 0) exit;_status = RUNING;}void join(){int n = pthread_join(_tid, nullptr);if(n != 0){cerr << "main thread join error:"  << _name << endl;return ;}_status = EXIT;}~Thread(){}public:pthread_t _tid;string _name;func_t _func; //线程未来要执行的回调ThreadStatus _status;
};

3.4V4版本

客户端线程化:构造一个线程专门去执行们收消息的任务

//udp_client.cc#include"udp_client.hpp"#include<pthread.h>using namespace std;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << "serverip  serverport  "<< "port\n" << endl;
}void* reciver(void* args)
{int sock = *(static_cast<int *>(args)); // 获取到对应的套接字while(true){// 收char buffer[2048];struct sockaddr_in temp;socklen_t len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);if (n > 0) // 收到了数据{buffer[n] = '\0';cout << buffer << endl;}}return nullptr;
}//./udp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1];  //获取ipuint16_t serverport = atoi(argv[2]);  //获取portint sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "create socket error " << std::endl;exit(SOCKET_ERR);}//client 这里要不要bind呢?要的!//要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。//为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS//那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的//同一家公司的port号,需要统一和规范的//明确server是谁struct sockaddr_in server;memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0server.sin_family = AF_INET; //指定我们的网络模式为ipv4server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给serverserver.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式pthread_t tid;pthread_create(&tid, nullptr, &reciver, &sock);while(true){//用户输入消息string message;std::cerr << "Please Enter Your Message#";//cin >> message;getline(cin, message);//什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文//发送给服务端sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}pthread_join(tid, nullptr);return 0;
}

3.5提供一个Windows客户端

我们这里实现一个windows的客户端,最终实现之后可以和Linux端一同通信。

代码只有四处不同:

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
#include<string>
#include<winsock2.h>#pragma comment(lib, "ws2_32.lib") //引入windows下的库using namespace std;int main()
{//用于检查WSADATA WSAData;if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0){printf("init error");return -1;}//....//用于结束对套接字库的使用。其主要目的是以恰当的方式关闭和清理套接字编程库//对于每个成功的WSAStartup调用,都必须有一个相应的WSACleanup调用,否则会导致资源泄漏WSACleanup();return 0;
}

完整代码:

#include<iostream>
#include<string>
#include<winsock2.h>
#include <WS2tcpip.h> // for inet_pton
#include <thread>#pragma comment(lib, "ws2_32.lib") //引入windows下的库using namespace std;int64_t serverport = 8888;string serverip = "117.72.37.100";void ReceiveData(SOCKET sock) {char buffer[2048];sockaddr_in temp;int len = sizeof(temp);while (true) {int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);if (n > 0) {buffer[n] = '\0';cout << buffer << endl;}}
}int main()
{//用于检查WSADATA WSAData;if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0){printf("init error");return -1;}int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){std::cerr << "create socket error " << std::endl;exit(-2);}//client 这里要不要bind呢?要的!//要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。//为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS//那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的//同一家公司的port号,需要统一和规范的//明确server是谁struct sockaddr_in server;memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0server.sin_family = AF_INET; //指定我们的网络模式为ipv4server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给server//server.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));thread receiveThread(ReceiveData, sock);cout << "Please Enter Your Message#";while (true){//用户输入消息string message;//cin >> message;getline(cin, message);//什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文//发送给服务端sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}receiveThread.join();//用于结束对套接字库的使用。其主要目的是以恰当的方式关闭和清理套接字编程库//对于每个成功的WSAStartup调用,都必须有一个相应的WSACleanup调用,否则会导致资源泄漏WSACleanup();return 0;
}

3.6客户端和服务端的源码:

  • server.hpp
#pragma once#include<iostream>
#include<cstring>
#include<strings.h>
#include<string>
#include<cstdio>
#include<functional>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unordered_map>
#include<pthread.h>
#include<vector>
#include"err.hpp"
#include"RingQueue.hpp"
#include"LockGuard.hpp"
#include"Thread.hpp"namespace ns_server
{const static uint16_t default_port = 8080; //设定一个端口号的默认值using func_t = std::function<std::string(std::string)>; //使用函数包装器,包装一个返回值和参数的都是string类型的函数class UdpServer{public:UdpServer(uint16_t port = default_port):port_(port){std:: cout << "server addr" << port_ << std::endl;pthread_mutex_init(&lock, nullptr);p = new Thread(1, std::bind(&UdpServer::Recv, this));c = new Thread(2, std::bind(&UdpServer::Broadcast, this));}void start(){//1.创建socket接口,打开网络文件//数据报套接字sock_ = socket(AF_INET, SOCK_DGRAM, 0);//返回的是一个网络文件的文件描述符if(sock_ < 0){std::cerr << "create socket error" << strerror(errno) << std::endl;exit(SOCKET_ERR); //用枚举构造的一个退出码}std::cout << "create socket success:" << sock_ << std::endl;//2.给服务器指明IP地址和端口号portstruct sockaddr_in local; //这个local,定义在用户空间特定的函数栈帧上,而不是内核里bzero(&local, sizeof(local)); //将local初始化为0//对local进行赋值local.sin_family = AF_INET;  //指定为Address_Familylocal.sin_port = htons(port_); //将主机字节序的port转化为对应的网络字节序// inet_addr: 1,2两个功能都具备// 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化// 2. 需要将主机序列转化成为网络序列// 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IPif(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0)  //绑定端口号{std::cerr << "bind socket error" << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind socket success:"<< sock_ << std::endl;p->run();c->run();}void addUser(const std::string& name, const struct sockaddr_in& peer){LockGuard lockguard(&lock);auto iter = onlineuser.find(name);if(iter == onlineuser.end())onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name , peer));}void Recv(){char buffer[1024]; //我们规定网络通信的时候最大时候是1024字节while(true){//收消息struct sockaddr_in peer;socklen_t len = sizeof(peer); // 这里一定要写清楚,未来缓冲区的大小int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 这里的-1为了兼容C向字符串的结尾加\0if(n > 0) buffer[n] = '\0'; //读取成功,设置\0else continue; //否则的话继续读取//拿到地址和端口,并且完成网络转主机化std::string clientip = inet_ntoa(peer.sin_addr);uint16_t clientport = ntohs(peer.sin_port); std::cout <<clientip << "-"<< clientport << "#" << buffer << std::endl;//构建一个用户并检查std::string name = clientip;name += "-";name += std::to_string(clientport);//如果不存在,就插入,如果存在什么都不做addUser(name, peer);std::string message = name + ">> " + buffer;rq.push(message);// //做业务处理// std::string reponse = service_(buffer);// //发消息// sendto(sock_, reponse.c_str(), reponse.size() , 0, (struct sockaddr *)&peer, sizeof(peer));}}void Broadcast(){while (true){std::string sendstring;rq.pop(&sendstring);std::vector<struct sockaddr_in> v; // 这里用vector优化一下效率,因为vector最终只在内存中拷贝数据{LockGuard lockguard(&lock);for (auto user : onlineuser){v.push_back(user.second);}}for (auto user : v){sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&user, sizeof(user));}}}   ~UdpServer() {pthread_mutex_destroy(&lock);c->join();p->join();delete c;delete p;}private:int sock_;uint16_t port_;//func_t service_; //std::unordered_map<std::string, struct sockaddr_in> onlineuser;RingQueue<std::string> rq;pthread_mutex_t lock;Thread *c;Thread *p;// std::string ip_;};}
  • server.cc
#include<iostream>
#include<memory>
#include<string>#include"udp_server.hpp"using namespace std;
using namespace ns_server;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << "port\n" << endl;
}//上层的业务处理, 不关心网络, 只负责业务处理
std::string transactionString(std::string requets)
{std::string result;char c;for(auto& r : requets){if(islower(r)){c = toupper(r);result.push_back(c);}elseresult.push_back(r);}return result;
}bool isPass(const std::string& command)
{   bool pass = true;auto pos = command.find("mv");if(pos != std::string::npos) pass = false;pos = command.find("rm");if(pos != std::string::npos) pass = false;// pos = command.find("");// if(pos != std::string::npos) pass = false;return pass;
}std::string excuteCommend(const std::string& command)
{//1.安全检查if(!isPass(command)) return "Bad command!";//2.业务处理FILE* fp = popen(command.c_str(), "r");if(fp == nullptr) return "None"; //3.获取结果char line[1024];std::string result;while(fgets(line, sizeof(line), fp) != NULL){result += line;}pclose(fp);return result;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);exit(USAGE_ERR);}//通过命令行参数获取端口号uint16_t port = atoi(argv[1]);// unique_ptr<UdpServer> server(new UdpServer("1.1.1.1", 8082));unique_ptr<UdpServer> server(new UdpServer(port));server->start();// server->Start();return 0;
}
  • client.hpp
#pragma once#include<iostream>
#include<memory>
#include<string>
#include<cstring>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>#include"err.hpp"
  • client.cc
#include"udp_client.hpp"#include<pthread.h>using namespace std;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << "serverip  serverport  "<< "port\n" << endl;
}void* reciver(void* args)
{int sock = *(static_cast<int *>(args)); // 获取到对应的套接字while(true){// 收char buffer[2048];struct sockaddr_in temp;socklen_t len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);if (n > 0) // 收到了数据{buffer[n] = '\0';cout << buffer << endl;}}return nullptr;
}//./udp_client serverip serverport
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1];  //获取ipuint16_t serverport = atoi(argv[2]);  //获取portint sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "create socket error " << std::endl;exit(SOCKET_ERR);}//client 这里要不要bind呢?要的!//要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。//为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS//那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的//同一家公司的port号,需要统一和规范的//明确server是谁struct sockaddr_in server;memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0server.sin_family = AF_INET; //指定我们的网络模式为ipv4server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给serverserver.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式pthread_t tid;pthread_create(&tid, nullptr, &reciver, &sock);while(true){//用户输入消息string message;std::cerr << "Please Enter Your Message#";//cin >> message;getline(cin, message);//什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文//发送给服务端sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}pthread_join(tid, nullptr);return 0;
}

到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正

在这里插入图片描述

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

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

相关文章

有线网络下windows电脑被投屏方案实践

最近在看使用笔记本屏幕作PC副屏的解决方案 无线网络Miracast 如果使用Win10/11自带的Miracast方案&#xff08;即windows系统中的&#xff1a;设置-系统-投影到此电脑&#xff09;&#xff0c;原则上需要通过Wi-Fi网络&#xff08;这是因为Miracast就是Wi-Fi联盟组织提出的&a…

react-native 搭建环境及运行项目

目前创建的是0.73版本的&#xff0c;Node 的版本应大于等于 18&#xff0c;需要 Java Development Kit [JDK] 17版本的&#xff08;必须是17版本&#xff09;。安装完后你可以在命令行中输入 javac -version&#xff08;请注意是 javac&#xff0c;不是 java&#xff09;来查看…

一键优化B2B2C电商系统,开启无限商机

在当今竞争激烈的电商行业&#xff0c;B2B2C模式已成为众多企业选择的经营模式之一。通过一键优化B2B2C电商系统&#xff0c;企业能够开启无限商机&#xff0c;实现更大的发展空间。 首先&#xff0c;优化B2B2C电商系统可以帮助企业提升用户体验&#xff0c;吸引更多客户。通过…

仿生蝴蝶制作——蝴蝶翅膀制作

前言 上一次已经设计好了的翅膀图纸 接下来就是根据这个图纸来制作翅膀。 过程中其实可以不用尺子准确测量&#xff0c;直接用碳纤维棒比着剪下来就好了&#xff0c;然后把减下来的一截比着剪下另一只翅膀需要的材料。因为左右两只翅膀差别不能太大&#xff0c;所以这样是最好…

异步编程和asyncio

介绍异步编程的重要性和在Python中的应用&#xff0c;特别是在I/O密集型任务和网络编程场景下。 目录 理解异步编程 异步编程基本概念 任务与Future 异步编程的工作原理 事件循环 协程&#xff08;Coroutines&#xff09; 异步与同步代码的结合 深入asyncio模块 事件循…

CCF-C推荐会议 IEEE CLOUD‘24 3月24日截稿!深圳开启全球云计算新纪元!

会议之眼 快讯 IEEE CLOUD(IEEE International Conference on Cloud Computing)即IEEE云计算国际会议将于 2024 年7月7日至13日在中国深圳举行&#xff01;IEEE CLOUD由lEEE Computer Society主办&#xff0c;CCF服务计算专委会、北京大学、IBM Research承办。CLOUD一直是研究人…

软件测试APP完整测试作业流程(附流程图),公司级软件测试流程化办公

目录 1. 概述 2. 软件测试流程 3. 软件测试周期人员活动图 4. 总结 1. 概述 1.1 目的 有效的保证软件质量&#xff1b; 有效的制定不同测试类型&#xff08;软件系统测试、音频主观性测试、Field Trial、专项测试、自动化测试、性 能测试、用户体验测试&#xff09;的软件…

【框架设计】MVC和MVVM对比图

1. MVC&#xff08;Model-View-Controller&#xff09; 单向通信View和Model通过Controller承上启下 2. MVVM&#xff08;Model-View-ViewModel&#xff09; 数据绑定M -> VM -> V DOM事件监听 V -> VM -> M 1. MVC是单向的&#xff0c;MVVM是双向的&#xff0c;…

【SpringCloud微服务全家桶学习笔记-GateWay网关(微服务入口)】

Gateway服务网关 API网关为微服务架构中的服务提供了统一的访问入口&#xff0c;客户端通过API网关访问相关服务。API网关的定义类似于设计模式中的门面模式&#xff0c;它相当于整个微服务架构中的门面&#xff0c;所有客户端的访问都通过它来进行路由及过滤。它实现了请求路…

二 超级数据查看器   讲解稿   导入功能

二 超级数据查看器 讲解稿 导入功能 APP下载地址 百度手机助手 下载地址4 ​ 讲解稿全文&#xff1a; 大家好。 今天我们对 超级数据查看器的 导入信息功能 做一下详细讲解。 首先&#xff0c;我们打开 超级数据查看器。 我们这个系统要实现的是&#xff0c;快速生…

GEE:基于Landsat8计算陆地表面温度(Land Surface Temperature,LST)

作者&#xff1a;CSDN _养乐多_ 本文将介绍在Google Earth Engine&#xff08;GEE&#xff09;平台上使用 Landsat 8 卫星影像数据计算陆地表面温度&#xff08;Land Surface Temperature&#xff0c;LST&#xff09;的代码。 结果如下图所示&#xff0c; 文章目录 一、参考…

阿里P8解析自动化测试工具 —— SeleniumAppium!

自动化测试&#xff0c;利用自动化测试工具&#xff0c;通过录制/编程方式实现测试活动&#xff0c;发现被测对象存在的缺陷&#xff0c;从而替代手工测试活动。自动化测试不局限于某个具体测试阶段&#xff0c;也不局限被测对象的类型&#xff0c;只要满足自动化测试的必要条件…

数据库--SQL语言-1

练习网站&#xff1a;自学SQL网 Select 查询语法复习 SELECT column, another_column, …FROM mytableWHERE condition AND/OR another_condition AND/OR …; 操作符号&#xff1a; 如果属性是字符串, 我们会用到字符串相关的一些操作符号&#xff0c;其中 LIKE&#xff08…

day04-Maven-SpringBootWeb入门

文章目录 01. Maven1.1 课程安排1.2 什么是Maven1.3 Maven的作用1.4 Maven模型1.5 Maven仓库1.6 Maven安装1.6.1 下载1.6.2 安装步骤 2 IDEA集成Maven2.1 配置Maven环境2.1.1 当前工程设置2.1.2 全局设置 2.2 创建Maven项目2.3 POM配置详解2.4 Maven坐标详解2.5 导入Maven项目 …

【axios】你的进度条准确吗

1、axios监听进度 上传和下载操作在前端中是非常常见的&#xff0c;当我们想知道上传或下载的进度时也不难&#xff0c;axios已经实现了监听进度的方法 import axios from axios// 上传请求 axios.post(/api/v1/upload, {data: xxx},{// onUploadProgress回调可以获取进度onU…

mysql 常用命令

1、显示锁的时间 show status like innodb_row_lock%;2、锁一行的方法 //开启 begin; //锁一行 select * from tbl_user where name 1aa1 for update;//解锁 commit;3、设置不自动提交 set autocommit 0; //自动提交 set autocommit 1;4、查看是否支持profile show vari…

2 月 Web3 游戏行业动态

作者&#xff1a;stellafootprint.network 数据来源&#xff1a;区块链游戏研究页面 - Footprint Analytics 2024 年 2 月&#xff0c;区块链游戏领域在加密货币价格上涨和活跃用户激增的推动下&#xff0c;实现了显著增长。然而&#xff0c;行业在维持用户参与度和留存率方面…

NodeJS实现线性查找算法

NodeJS实现线性查找算法 以下是使用 Node.js 实现线性搜索算法的示例代码&#xff1a; function linearSearch(arr, target) {for (let i 0; i < arr.length; i) {if (arr[i] target) {return i; // 如果找到目标&#xff0c;返回索引}}return -1; // 如果未找到目标&am…

微服务配置中心

什么是配置中心 配置中心是一种用于管理应用程序或系统配置信息的中央服务。它允许开发人员在多个环境&#xff08;如开发、测试、生产&#xff09;之间共享配置&#xff0c;并且可以在不停止应用程序的情况下动态更新配置。 配置中心是统一管理各种应用配置的工具。它能够集中…

【Ubuntu】原生Ubuntu-dock 栏 安装与卸载

1.查看是否安装 Ubuntu-dock&#xff08;新版本的Ubuntu自带Ubuntu-dock version> 18.04&#xff09; gnome-extensions list 2.安装Ubuntu-dock sudo apt install gnome-shell-extension-ubuntu-dock 3.重启&#xff0c;一定要重启&#xff01;&#xff01;&#xff01;…