Linux学习记录——삼십 socket编程---udp套接字

文章目录

  • UDP套接字简单通信
    • 1、服务端
      • 1、创建文件,写框架
      • 2、用命令行参数调起程序
      • 3、服务端运行逻辑
    • 2、客户端
      • 1、创建套接字
      • 2、发送数据
    • 3、测试
    • 4、通信
    • 5、加功能
      • 1、处理数据
      • 2、群聊
    • 6、Windows下socket编程的不同


UDP套接字简单通信

1、服务端

1、创建文件,写框架

接下来通过代码来理解套接字。先写一个echo server的代码,一个客户端,一个服务端,客户端发消息,服务端接收后再返回来。

创建套接字函数

在这里插入图片描述

domain用来表示要进行什么通信,传AF_INET AF_UNIX,type对应的套接字种类

在这里插入图片描述

其中第二个SOCK_DGRAM就是指数据报,第一个就是TCP协议,protocol默认为0,会自动推演出用什么协议,TCP或者UDP。返回值,成功返回文件描述符,失败返回-1并设置错误码

在这里插入图片描述

初始化时用这个接口。我们创建四个文件,两个client,两个server,udp_client.cc,udp_client.hpp,udp_server.cc,udp_server.hpp。

makefile

.PHONY: all
all:udp_client udp_serverudp_client:udp_client.ccg++ -o $@ $^ -std=c++11 udp_server:udp_server.ccg++ -o $@ $^ -std=c++11 .PHONY: clean
clean:rm -f udp_client udp_server

在server的头文件中

#pragma once#include <iostream>
#include <memory>
#include <sys/type.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>namespace ns_server
{enum{SOCKET_ERR=1,//返回时用枚举里的错误码,这样用户就知道是什么原因错误的};class UdpServer{public:UdpServer(){}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 succeed: " << sock_ << std::endl;//默认文件描述符应当是3}void Start(){}~UdpServer(){}private:int sock_;};
}

接下来给服务器指明IP地址和端口号,用到bind函数。

在这里插入图片描述

sockfd就是刚才用socket函数创建的套接字。第二个参数addr,因为是网络通信,我们要用的是AF_INET。传参数前,我们需要填充一下IP地址和端口号。

在这里插入图片描述

in_port_t是重命名了uint16_t,sin_port就是端口号;sin_addr是IP地址,是一个in_addr结构体类型,其实这个结构体只有一个uint32_t类型的变量,32位的整数;剩下的sin_zero是一个填充字段。而AF_INET在最一开始的__SOCKADDR_COMMON里。

在这里插入图片描述

里面的双井号,传进来一个sin_后,sa_prefix就是sin_,双井号后是family,它们就会被组合起来,最终就是sa_family_t sin_family,sa_family_t是一个整数类型。

接下来要填充这个结构体,首先要清空,也可以不清空,清空用bzero,可以把缓冲区写为0。

在这里插入图片描述

我们在类内定义一个端口号port_,填充端口号时,不能直接给port_,因为这台主机不仅发消息,还得接收消息,接收消息的话,端口号就得在网络中,被其它主机知晓,而这个port_是在类内定义的,所以需要本地序列号转网络序列号。

#pragma once#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <strings.h>
#include <cstdlib>
#include <netinet/in.h>
#include <arpa/inet.h>namespace ns_server
{enum{SOCKET_ERR=1,BIND_ERR,};const static int default_port = 8080;class UdpServer{public:UdpServer(std::string ip, uint16_t port = default_port): ip_(ip), port_(port){std::cout << "server addr: " << ip << " : " << port_ << std::endl;}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 succeed: " << sock_ << std::endl;//默认文件描述符应当是3//2. 给服务器指明IP地址Port号struct sockaddr_in local;//网络通信要引入头文件<netinet/in.h>, <arpa/inet.h>bzero(&local, sizeof(local));local.sin_family = AF_INET;//上面的AF_INET用来创建套接字,这个用来初始化sockaddr_in结构体local.sin_port = htons(port_);//端口号的主机转网络序列的接口//字符串转为4字节int,不能强转,需要转化,并且转化完后要再变成网络序列//in_addr_t inet_addr函数就可以解决这个问题,它的作用就是字符串转4字节int并转网络序列,传一个const char*的就行local.sin_addr.s_addr = inet_addr(ip_.c_str());//现在要把这个local绑定到套接字中,但这个local是在函数内的,也就是临时变量,它在用户空间的特定函数的栈帧上,不在内核中if(bind(sock_, (struct sockaddr*)&local, sizeof(local))){std::cerr << "bind socket error: " << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind socket succeed: " << sock_ << std::endl;}void Start(){}~UdpServer(){}private:int sock_;unit16_t port_;std::string ip_;};
} 

udp_server.cc文件里这样写

#include "udp_server.hpp"
using namespace std;
using namespace ns_server;int main()
{unique_ptr<UdpServer> usvr(new UdpServer("1.1.1.1", 8082));usvr->InitServer();usvr->Start();return 0;
}

但是运行起来后会出现绑定错误。我们把1.1.1.1换成自己的云服务器的公网IP后,它依旧报错:Cannot assign requested address。

2、用命令行参数调起程序

云服务器不需要绑定IP地址,我们要让云服务器自己去指向IP地址。而自己本地装的虚拟机或者物理机器是允许用户去绑定IP地址的。这样的话我们就写成命令行参数的形式,main里只用端口号,UdpServer这个类里就不去初始化ip地址。为了方便,把报的错误统一放到一个头文件中。这样改动后,UdpServer的类里还需要绑定IP地址,现在已经没有ip_这个成员变量了,那么如何去绑定?云服务器不应当绑定某一个IP,因为云服务器可能有多个IP地址,如果只绑定一个,那就只能收到这一个的数据报,我们应当接收所有发送到这个主机的数据,根据端口,发到这个端口的数据会被使用。

err.hpp

#pragma onceenum
{USAGE_ERR=1,SOCKET_ERR,BIND_ERR, 
};

udp_server.cc

#include "udp_server.hpp"
#include <string>
using namespace std;
using namespace ns_server;static void usage(string proc)
{std::cout << "Usage:\n\t" << proc << " port\n"  << std::endl;
}//既然不传IP地址,那就将main变成带参的,我们手动写上端口号,里面也需要做改动
//像这样写来启动程序 ./udp_server port
int main(int argc, char* argv[])
{if(argc != 2)//上面写启动程序那里,是两个命令行参数,所以这里是2{usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);//unique_ptr<UdpServer> usvr(new UdpServer("1.1.1.1", 8082));unique_ptr<UdpServer> usvr(new UdpServer(port));usvr->InitServer();usvr->Start();return 0;
}

udp_server.hpp

在这里插入图片描述

这样改完后,我们写./udp_server 8080就可以接收8080这个端口号的数据。只是现在服务器还没写运行逻辑,所以只是能绑定成功,并没有看到数据处理的过程,接下来写这部分。

3、服务端运行逻辑

recvfrom接口,从一个套接字获取数据。

在这里插入图片描述

sockfd就是我们代码中的sock_,用socket接口创建好后返回的东西。buf是自己定义的缓冲区,把数据放在这里,len是缓冲区长度,flags是读取方式,默认设为0,阻塞方式。接口返回值是实际读了多少数据,len是整个长度。src_addr和addrlen是输入输出型参数,这个接口既然是接收数据用的,那么就得知道是谁发过来数据的,在网络中,网络间通信就是进程间通信,要标识是哪一个进程,就得知道它所在的主机,这需要IP地址,还要知道是哪一个进程,这需要端口号(Port),所以src_addr这个参数,我们用这个接口的时候就得传入客户端的IP和Port号,因为是服务端调用这个接口,这个接口输出时得返回一个结构体,里面有客户端的IP和Port号,addrlen就是实际结构体的大小。

        void Start(){char buffer[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);if(n > 0) buffer[n] = '\0';else continue;}}

recvfrom那里写sizeof(buffer) - 1,是因为考虑到这些原因。数据具体是什么形式的,由程序员定义,这里不涉及这些,因为这些系统接口是C写的,我们要把数据当作字符串使用,就需要最后一个位置是\0,这个位置得我们自己写上,所以得预留出这个位置,所以要-1。后面返回值大于0时,接口调用没有错误,那么就在最后一个位置加上\0结尾,整体就是一个字符串了。

接收后服务端也不知道这是谁的IP和Port号,我们需要提取出来。

                //提取client信息std::string clientip = inet_ntoa(peer.sin_addr);uint16_t clientport = ntohs(peer.sin_port);std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;

接收完后要发送数据,用sendto接口。

在这里插入图片描述

buf就是发送的数据所在的缓冲区。服务端接收客户端的数据,然后再发送给客户端,所以dest_addr也是客户端的IP和Port号。

sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, sizeof(peer));

这里的strlen(buffer)这样写是因为,无论是向谁写,比如这里是向套接字,Linux下一切皆文件,文件中存储的可不是C语言中的字符串形式,所以不需要最后的\0。

程序运行起来后,服务端一直在运行中,我们可以用netstat -naup来查看,n是把所有能显示成数字的显示出来,u是显示udp,p显示进程信息,a代表所有进程。进行查看时,会发现一个新建立的PID,端口号就是调用程序时自己写的端口号,前面有一个随机绑定的IP地址,是全0的,就相当于给这个IP地址做了一个定义,只要发到这个端口号的数据,都会发到这个主机上。

hpp

#pragma once#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <string>
#include <strings.h>
#include <cstdlib>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"namespace ns_server
{const static int default_port = 8080;class UdpServer{public:UdpServer(uint16_t port = default_port): port_(port){std::cout << "server addr: " << port_ << std::endl;}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 succeed: " << sock_ << std::endl;//默认文件描述符应当是3//2. 给服务器指明IP地址Port号struct sockaddr_in local;//网络通信要引入头文件<netinet/in.h>, <arpa/inet.h>bzero(&local, sizeof(local));local.sin_family = AF_INET;//上面的AF_INET用来创建套接字,这个用来初始化sockaddr_in结构体local.sin_port = htons(port_);//字符串转为4字节int,不能强转,需要转化,并且转化完后要再变成网络序列//in_addr_t inet_addr函数就可以解决这个问题,它的作用就是字符串转4字节int并转网络序列,传一个const char*的就行//local.sin_addr.s_addr = inet_addr(ip_.c_str());//云服务器,或者一款服务器,一般不要指明某一个确定的IPlocal.sin_addr.s_addr = INADDR_ANY;//让我们的udpserver在启动的时候,绑定本主机上的任意一个IP地址//现在要把这个local绑定到套接字中,但这个local是在函数内的,也就是临时变量,它在用户空间的特定函数的栈帧上,不在内核中if(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0){std::cerr << "bind socket error: " << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind socket succeed: " << sock_ << std::endl;}void Start(){char buffer[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);if(n > 0) buffer[n] = '\0';else continue;//提取client信息std::string clientip = inet_ntoa(peer.sin_addr);uint16_t clientport = ntohs(peer.sin_port);std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;//发送sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, sizeof(peer));}}~UdpServer(){}private:int sock_;uint16_t port_;//std::string ip_;};
} 

cc

#include "udp_server.hpp"
#include <string>
using namespace std;
using namespace ns_server;static void usage(string proc)
{std::cout << "Usage:\n\t" << proc << " port\n"  << std::endl;
}//既然不传IP地址,那就将main变成带参的,我们手动写上端口号,里面也需要做改动
//像这样写来启动程序 ./udp_server port
int main(int argc, char* argv[])
{if(argc != 2)//上面写启动程序那里,是两个命令行参数,所以这里是2{usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);//unique_ptr<UdpServer> usvr(new UdpServer("1.1.1.1", 8082));unique_ptr<UdpServer> usvr(new UdpServer(port));usvr->InitServer();usvr->Start();return 0;
}

err.hpp

#pragma onceenum
{USAGE_ERR=1,SOCKET_ERR,BIND_ERR, 
};

2、客户端

1、创建套接字

udp_client.hpp

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>

udp_client.cc

#include "udp_client.hpp"
#include "err.hpp"int main() 
{int sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "create socket error" << std::endl;exit(SOCKET_ERR);}return 0;
}

客户端也需要绑定,因为网络通信就是客户端和服务端互相标识对方唯一的进程后,进行网络版本的进程间通信,所以需要标识,但不用我们自己去标识。绑定是一个系统调用,客户端的端口号绑定由操作系统来自动绑定,防止客户端自己绑定端口号会导致两个软件绑定同一个端口号,那就不能同时打开。

既然这样,为什么服务端要自己绑定端口号?服务器的端口号不能自己改变,它要服务于众多客户,就像一个官方一样,它不能随便更改。但服务端也有很多,为什么可以自己定端口号?客户端来自众多源头,但是服务端由一家公司自己制定的,所以公司自己来规定好不冲突就行。

2、发送数据

套接字创建完,客户端就可以开始发送数据了。用sendto来发消息,但是这时候我们需要知道服务端的IP和Port号。服务端是临时的一个IP地址,这如何获取?客户端在调起运行时,也是写命令行参数,要写ip和端口号,ip其实用服务器的公网IP就可以传送过去数据,端口号就是我们自己定的那个。

udp_client.cc

#include "udp_client.hpp"
#include "err.hpp"static void usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip  serverport\n"  << std::endl;
}int main(int argc, char* argv[]) 
{if(argc != 3){usage(argv[0]);exit(USAGE_ERR);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);int sock = socket(AF_INET, SOCK_DGRAM, 0); if(sock < 0){std::cerr << "create socket error" << std::endl;exit(SOCKET_ERR);}//客户端由系统自己来绑定端口号//明确服务器是谁,填充sockaddr这个结构体struct sockaddr_in server;memset(&server, 0, sizeof(server));//就和bzero一样server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());while(true){//用户输入std::string message;std::cout << "Please Enter# ";std::cin >> message;//发送sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));//接收char buffer[2048];struct sockaddr_in temp;//temp就是上面server结构体的结果socklen_t len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);//recvfrom通常用于udp套接字的接收数据,具体用法man一下if(n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}return 0;
}

操作系统什么时候绑定客户端?在我们首次系统调用发送数据的时候,操作系统会在底层随机选择客户端端口号,加上自己的ip,绑定并构建要发送的数据报文。

3、测试

测试的时候要开两个窗口,在客户端写./udp_client 127.0.0.1 8081,127.0.0.1是本地环回,表达的是当前主机,通常用来进行本地通信或者测试。也是每一台机器默认的IP地址,用它来测试的时候,就走网络协议栈,但不会把数据发送到网络中。

测试的时候客户端发送消息,服务端能接收消息,那么软件没问题,如果实际中没有通过,那就说明是网络问题。

sz udp_client,也就是sz后面跟上客户端可执行文件,就可以把客户端放到桌面上。

4、通信

想通过现在写的代码来进行通信,sz udp_client就会弹出一个窗口,可以选择把这个客户端下载到哪里,然后启动另一个云服务器,输入指令rz加上一个空格,按回车,就可以选择客户端把它下载到当前云服务器中,但是我们还需要chmod +x udp_client,因为默认没有运行权限。想要启用这个客户端,我们先./udp_client,如果打印出来语句后,说明能正常运行,之后需要服务端的IP地址和端口号,输入指令./udp_client IP Port就可以运行客户端了,在客户端输入消息,服务端就会出现对应的消息,服务端在每条信息前面也会打印出来客户端的IP地址和随机分配给该客户端的端口号。让服务端停止运行,服务就挂掉了,客户端发信息就没用了。

5、加功能

1、处理数据

加上一个包装器functional,把函数作为类内成员初始化,服务端发送经过函数处理过的数据,而这个函数要在类外传进来,此时这个函数就不需要考虑网络,只考虑数据就行。

udp_server.hpp

#include <cstdio>const static int default_port = 8080;using func_t = std::function<std::string(std::string)>;class UdpServer{public:UdpServer(func_t cb, uint16_t port = default_port): service_(cb), port_(port){std::cout << "server addr: " << port_ << std::endl;}//......//做业务处理std::string response = service_(buffer);//发送sendto(sock_, response.c_str(), response.size(), 0, (struct sockaddr*)&peer, sizeof(peer));//......private:int sock_;uint16_t port_;func_t service_;//网络服务器要进行业务处理//std::string ip_;};

udp_server.cc

//上层的业务处理,不关系网络发送,只负责信息处理即可
std::string transactionString(std::string request)//request就是一个string
{std::string result;char c;for(auto &r : request){if(islower(r)){c = toupper(r);result.push_back(c);}elseresult.push_back(r);;}return result;
}//既然不传IP地址,那就将main变成带参的,我们手动写上端口号,里面也需要做改动
//像这样写来启动程序 ./udp_server port
int main(int argc, char* argv[])
{if(argc != 2)//上面写启动程序那里,是两个命令行参数,所以这里是2{usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);//unique_ptr<UdpServer> usvr(new UdpServer("1.1.1.1", 8082));unique_ptr<UdpServer> usvr(new UdpServer(transactionString, port));//这里就要传这个函数usvr->InitServer();usvr->Start();return 0;
}

这个功能比较简单。写一个服务端执行客户端命令的函数

在这里插入图片描述

这个接口会执行command参数指定的命令,执行完后以文件指针的类型返回,所以调用者要用文件操作才行,type则是用什么类型的文件操作,读写,追加。调用这个函数后,函数内部会创建管道,创建子进程,把处理后的结果定向到要返回的文件指针,让用户能够以读的方式来获取结果。用fgets函数来获取结果,从文件流中获取数据,并且因为是c接口,所以读取的数据自动转换成字符串形式。

在这之前,我们要先防止一些命令的实现。

static bool isPass(const std::string &command)
{bool pass = true;auto pos = command.find("rm");if(pos != std::string::npos) pass = false;pos = command.find("mv");if(pos != std::string::npos) pass = false;return pass;
}//客户端传过来命令,服务端执行
std::string excuteCommand(std::string command)
{//1. 安全检查if(!isPass(command)) return "Error!";//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;
}

这样客户端就可以给服务端发命令了,发过来的命令会被执行。但是还有问题,ls -a -l不会正确显示结果,这是因为udp_client.cc文件里用户输入部分不够完善,cin会以空格为分隔符。

        //用户输入std::string message;std::cout << "Please Enter# ";std::cin >> message;

换成getline就好

std::getline(std::cin, message);

现在只能执行单进程的命令,加上多个进程的。

    pos = command.find("while");if(pos != std::string::npos) pass = false;

加上防止被杀。

    pos = command.find("kill");if(pos != std::string::npos) pass = false;

2、群聊

服务端收到一个信息就把信息给所有的客户端。客户端给消息,服务端作为中间人,经过处理后再把消息发给所有客户端,其实这就是生产消费模型。要发给所有人,就需要所有人的套接字信息。 分成两个线程,一个收消息,另一个发消息,消息放在环形队列中,再给客户端。这里用到之前的RingQueue.hpp文件。

#pragma once#include <iostream>
#include <vector>
#include <pthread.h>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include <semaphore.h>
#include <string>
#include <cstring>static const int N = 50;//从5改成了50template<class T>
class RingQueue
{
private:void P(sem_t &s) {sem_wait(&s); }void V(sem_t &s) {sem_post(&s); }//发布信号量的接口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): _ring(num), _cap(num){sem_init(&_data_sem, 0, 0);sem_init(&_space_sem, 0, num);_c_step = _p_step = 0;pthread_mutex_init(&_c_mutex, nullptr);pthread_mutex_init(&_p_mutex, nullptr);}//生产void push(const T& in){P(_space_sem);//P操作,生产者需要看看空间信号量是否不为空,不空才可以继续Lock(_p_mutex);//不需要判断,一定有对应的空间资源给我//因为信号量本身就是描述临界资源的,它可以在临界区外去申请,P成功就说明可以继续执行了_ring[_p_step] = in;//_p_step是生产者的位置++_p_step;_p_step %= _cap;Unlock(_p_mutex);//V操作V(_data_sem);//一个数据放进去了,那么数据信号量就增加}//消费void pop(T* out){P(_data_sem);//P操作,消费者需要看看数据信号量是否不为空,不空才可以继续Lock(_c_mutex);*out = _ring[_c_step];//_c_step是消费者的位置++_c_step;_c_step %= _cap;Unlock(_p_mutex);V(_space_sem);//一个数据被拿走,消费者往后走一步,空间信号量就减少}~RingQueue(){sem_destroy(&_data_sem);sem_destroy(&_space_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);}
private:std::vector<T> _ring;int _cap;//环形队列大小sem_t _data_sem;//只有消费者关心sem_t _space_sem;//只有生产者关心int _c_step;//消费者位置int _p_step;//生产者位置pthread_mutex_t _c_mutex;//消费者之间的锁pthread_mutex_t _p_mutex;//生产者之间的锁
};

让原先的Start函数改名成Recv,它来接收消息,创建一个Broadcast的函数来发消息。定义一个map来存储客户端,将接收客户端的套接字结构体和客户端的IP、Port绑定在一起。处理数据前先查明是否存在于map,存在就不做什么,不存在就插入,然后再去处理数据。不过这里就不处理数据,把接收到的客户端数据放到环形队列中。发送数据就放到一个别的函数中来做。

    //类前面const static int default_port = 8080;using func_t = std::function<std::string(std::string)>;//....void addUser(const std::string &name, const struct sockaddr_in &peer){auto iter = onlineuser.find(name);if(iter != onlineuser.end()) return;onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));}void Recv(){char buffer[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);if(n > 0) buffer[n] = '\0';else continue;//提取client信息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);rq.push(buffer);//做业务处理//std::string message = service_(buffer);}}void Broadcast()//接收消息是while(true)来死循环接收,发送消息也要这样{while(true){std::string sendstring;rq.pop(&sendstring);for(auto user : onlineuser){std::cout << "Broadcast message to " << user.first << sendstring << std::endl;sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));}}}~UdpServer(){}private:int sock_;uint16_t port_;func_t service_;//网络服务器要进行业务处理std::unordered_map<std::string, struct sockaddr_in> onlineuser;RingQueue<std::string> rq;//std::string ip_;};
} 

onlineuser需要受到保护,防止被不知名的客户端插入给更改,所以加锁,引入之前写的LockGuard.hpp。

LockGuard.hpp

#pragma once#include <iostream>
#include <pthread.h>class Mutex//自己不维护锁,由外部传入
{
public:Mutex(pthread_mutex_t *mutex):_pmutex(mutex){} void lock(){pthread_mutex_lock(_pmutex);}void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}
private:pthread_mutex_t *_pmutex;
};class LockGuard//自己不维护锁,由外部传入
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}
private:Mutex _mutex;
};
        UdpServer(func_t cb, uint16_t port = default_port): service_(cb), port_(port){std::cout << "server addr: " << port_ << std::endl;pthread_mutex_init(&lock, nullptr);}void addUser(const std::string &name, const struct sockaddr_in &peer){LockGuard lockguard(&lock);auto iter = onlineuser.find(name);if(iter != onlineuser.end()) return;onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));}void Broadcast(){std::string sendstring;rq.pop(&sendstring);LockGuard lockguard(&lock);for(auto user : onlineuser){std::cout << "Broadcast message to " << user.first << sendstring << std::endl;sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));}}~UdpServer(){pthread_mutex_destroy(&lock);}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;//std::string ip_;};

但Broadcast函数那里加锁并不是最优解。我们需要保护的是客户端的套接字,而不是发送的这个过程,这里可以这样写

        void Broadcast(){while(true){std::string sendstring;rq.pop(&sendstring);std::vector<struct sockaddr_in> v;{LockGuard lockguard(&lock);for(auto user: onlineuser){v.push_back(user.second);}}for(auto user : v){//std::cout << "Broadcast message to " << user.first << sendstring << std::endl;sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr*)&(user), sizeof(user));}}}

拷贝到v里是用范围for,是内存级拷贝,是STL容器进行的拷贝,消耗少。而sendto是系统调用接口,有IO开销。

写完这些后,还要支持多线程,引入之前写的Thread.hpp,并且还要改一下多线程执行的方法。

以前的Thread.hpp

#pragma once#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
using namespace std;typedef void (*func_t)();class Thread
{
public:typedef enum{NEW = 0,RUNNING,EIXTED}ThreadStatus;typedef void (*func_t)(void*);
public:Thread(int num, func_t func, void* args):_tid(0), _status(NEW), _func(func), _args(args)//num是线程编号{char name[128];snprintf(name, sizeof(name), "thread-%d", num);_name = name;}int status() {return _status;}string threadname() {return _name;}pthread_t threadid(){if(_status == RUNNING) return _tid;else{return 0;}}//runHelper是成员函数,类的成员函数具有默认参数this,也就是括号有一个Thread* this,pthread_create的参数应当是void*类型,就不符合,所以加上static,放在静态区,就没有this,但是又有新问题了//static成员函数无法直接访问类内属性和其他成员函数,所以create那里要传this指针,this就是当前线程对象,传进来才能访问类内的东西static void* runHelper(void* args)//args就是执行方法的参数{Thread* ts = (Thread*)args;//就拿到了当前对象//函数里可以直接调用func这个传过来的方法,参数就是_args(*ts)();//调用了func函数return nullptr;}void operator()()//仿函数{if(_func != nullptr) _func(_args);}void run()//run函数这里传方法{int n = pthread_create(&_tid, nullptr, runHelper, this);//为什么传this?if(n != 0) exit(1);_status = RUNNING;}void join(){int n = pthread_join(_tid, nullptr);if(n != 0){cerr << "main thread join thread" << _name << "error" << endl;return ;} _status = EXITED;}~Thread(){}
private:pthread_t _tid;string _name;func_t func;//线程未来要执行的函数方法void* _args;//也可以不写这个,那么typedef的函数指针就没有参数。当然,参数也可用模板来写ThreadStatus _status;
};

改后的,不需要传参了,让线程执行接收和发送数据的工作,所以初始化这里就传进来编号和函数即可。

#pragma once#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
#include <functional>
using namespace std;typedef void (*func_t)();class Thread
{
public:typedef enum{NEW = 0,RUNNING,EXITED}ThreadStatus;//typedef void (*func_t)(void*);using func_t = function<void ()>;
public:Thread(int num, func_t func):_tid(0), _status(NEW), _func(func)//num是线程编号{char name[128];snprintf(name, sizeof(name), "thread-%d", num);_name = name;}int status() {return _status;}string threadname() {return _name;}pthread_t threadid(){if(_status == RUNNING) return _tid;else{return 0;}}//runHelper是成员函数,类的成员函数具有默认参数this,也就是括号有一个Thread* this,pthread_create的参数应当是void*类型,就不符合,所以加上static,放在静态区,就没有this,但是又有新问题了//static成员函数无法直接访问类内属性和其他成员函数,所以create那里要传this指针,this就是当前线程对象,传进来才能访问类内的东西static void* runHelper(void* args)//args就是执行方法的参数{Thread* ts = (Thread*)args;//就拿到了当前对象//函数里可以直接调用func这个传过来的方法,参数就是_args(*ts)();//调用了func函数return nullptr;}void operator()()//仿函数{if(_func != nullptr) _func();}void run()//run函数这里传方法{int n = pthread_create(&_tid, nullptr, runHelper, this);//为什么传this?if(n != 0) exit(1);_status = RUNNING;}void join(){int n = pthread_join(_tid, nullptr);if(n != 0){cerr << "main thread join thread" << _name << "error" << endl;return ;} _status = EXITED;}~Thread(){}
private:pthread_t _tid;string _name;func_t _func;//线程未来要执行的函数方法//void* _args;//也可以不写这个,那么typedef的函数指针就没有参数。当然,参数也可用模板来写ThreadStatus _status;
};

udp_server.hpp那里,两个线程c和p,把它设置成指针的,那么成员就是Thread* c和Thread* p,初始化时new出来,传进去的函数应当是std库里的bind接口

template< class F, class… Args >
std::function<F(Args…)> std::bind( F&& f, Args&&… args );

F 是一个可调用对象的类型,可以是函数指针、函数对象、成员函数指针等。
Args… 是 F 所接受的参数类型。

这样就可以把接收和发送数据的函数传进去了。原先的InitServer函数,在最后加上c->run(),p->run(),让两个线程运行起来,析构函数那里也要加上两个线程指针的析构。

        UdpServer(uint16_t port = default_port): port_(port){std::cout << "server addr: " << port_ << std::endl;pthread_mutex_init(&lock, nullptr);c = new Thread(1, std::bind(&UdpServer::Recv, this));p = new Thread(1, 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 succeed: " << sock_ << std::endl;//默认文件描述符应当是3//2. 给服务器指明IP地址Port号struct sockaddr_in local;//网络通信要引入头文件<netinet/in.h>, <arpa/inet.h>bzero(&local, sizeof(local));local.sin_family = AF_INET;//上面的AF_INET用来创建套接字,这个用来初始化sockaddr_in结构体local.sin_port = htons(port_);//字符串转为4字节int,不能强转,需要转化,并且转化完后要再变成网络序列//in_addr_t inet_addr函数就可以解决这个问题,它的作用就是字符串转4字节int并转网络序列,传一个const char*的就行//local.sin_addr.s_addr = inet_addr(ip_.c_str());//云服务器,或者一款服务器,一般不要指明某一个确定的IPlocal.sin_addr.s_addr = INADDR_ANY;//让我们的udpserver在启动的时候,绑定本主机上的任意一个IP地址//现在要把这个local绑定到套接字中,但这个local是在函数内的,也就是临时变量,它在用户空间的特定函数的栈帧上,不在内核中if(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0){std::cerr << "bind socket error: " << strerror(errno) << std::endl;exit(BIND_ERR);}std::cout << "bind socket succeed: " << sock_ << std::endl;p->run();c->run();}~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_;};

makefile里,因为服务端用了多线程,所以要在后面加上-lpthread。启动服务端后,用ps -aL |grep udp_server来查看,会发现有三个线程,接收,发送和主线程。

实际运行起来后会发现,客户端只能发一个消息才能接收一个服务端的消息,这是因为客户端是阻塞式运行的,只能先发再收,所以客户端也要改成多线程的。

把接收消息的部分放进一个函数里

void* recver(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;std::cout << buffer << std::endl;//往1号描述符输出}}
}
//......pthread_t tid;pthread_create(&tid, nullptr, recver, &sock);while(true){//用户输入std::string message;std::cerr << "Please Enter# ";//往2号文件描述符输出std::getline(std::cin, message);//发送sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}

接收和输入用两个输出流。

开启服务端后,我们输入命令mkfifo message_pipe,就创建了一个命名管道。将客户端的结果重定向到管道,./udp_client 127.0.0.1 8081 > message_pipe,那么客户端发消息,服务端收到,再发送给客户端,这个消息就会被放到管道,服务端发来的消息从管道中能够读取出来,但是读取出来的消息并不知道是哪个客户端发的,再改一下代码。

udp_server.hpp中有发消息者的信息

//构建一个用户,并检查std::string name = clientip;name += "-";name += std::to_string(clientport);//如果不存在,就插入;如果存在就什么都不做addUser(name, peer);rq.push(buffer);

原本是消息放到buffer里,然后把buffer放到环形队列中,现在加工一下

                //构建一个用户,并检查std::string name = clientip;name += "-";name += std::to_string(clientport);//如果不存在,就插入;如果存在就什么都不做addUser(name, peer);std::string message = name + " >>" + buffer;rq.push(message);

这样服务端发送消息前这个消息就带上了源头信息。

6、Windows下socket编程的不同

需要引入头文件WinSock2.h。

#pragma comment(lib, "ws2_32.lib")//引入一个库WSADATA WSAData;
if(WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
{std::cout << "init error" << std::endl;return -1;
}
//......最后
WSACleanup();

WSADATA全拼就是windows socket addr data,用这个MAKEWORD宏来构建2.2版本,把这个版本的数据结果放到WSAData中,WSAStartup用来检查引入的库和期望用的版本是否一致,不一致就打印init error。WSACleanup就是把程序在库中打开的各种数据,用到的东西给清理掉,然后结束程序。

其它部分都和上面没有什么不同。

#include <iostream>
#include <string>
#include <WinSock2.h>#pragma comment(lib, "ws2_32.lib")uint16_t serverport = 8080;//把服务端ip和端口号单独拎出来,这样就直接用就好,改的时候改这里
std::string serverip = "106.75.12.79";int main()
{WSADATA WSAData;if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0){std::cout << "init error" << std::endl;return -1;}SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){std::cerr << "create socket error" << std::endl;exit(-2);}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){std::string message;std::cout << "Please Enter# ";std::getline(std::cin, message);sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));char buffer[2048];struct sockaddr_in temp;int len = sizeof(temp);int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;//往1号描述符输出}}WSACleanup();return 0;
}

运行起来时会报错,这一行有错误server.sin_addr.s_addr = inet_addr(serverip.c_str()),需要用inet_pton(),我们也可以屏蔽这条警告。

在这里插入图片描述

#include <iostream>
#include <string>
#include <winsock2.h>#pragma warning(disable:4996)//屏蔽警告,让编译器自行解决
#pragma comment(lib, "ws2_32.lib")

也可以解决它。inet_pton这个函数将IP地址转网络地址,需要用到头文件WS2tcpip.h,int inet_pton(int af, const char *src, void *dst)。第一个参数填通信方式,第二个参数填IP地址,第三个参数是一个用来存放转换后的网络地址的缓冲区。

inet_pton(AF_INET, "106.75.12.79", &(server.sin_addr));

运行起来就可以创建出一个客户端窗口。

Linux Udp部分

Windows Udp部分

下一篇写TCP套接字通信。

结束。

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

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

相关文章

vue+element plus 使用table组件,清空用户的选择项

<el-table ref"tableRef"> .... </el-table> <script lang"ts" setup> import { onMounted, reactive, ref, nextTick } from vue const clearBtn () > {console.log(清空用户的选择项)tableRef.value.clearSelection() } </scr…

基于Java的酒店管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

docker alpine:3.16 root权限安装Anaconda3-2020.07-Linux-x86_64和jdk

首先查看系统版本: rootfv-az454-287:/tmp# uname -a Linux fv-az454-287 5.15.0-1046-azure #53~20.04.1-Ubuntu SMP Mon Aug 28 14:17:23 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux rootfv-az454-287:/tmp# grep NA /etc/os-release NAME"Ubuntu" PRETTY_NAME&q…

在c#中使用CancellationToken取消任务

目录 &#x1f680;介绍&#xff1a; &#x1f424;简单举例 &#x1f680;IsCancellationRequested &#x1f680;ThrowIfCancellationRequested &#x1f424;在控制器中使用 &#x1f680;通过异步方法的参数使用cancellationToken &#x1f680;api结合ThrowIfCancel…

Docker Compose初使用

简介 Docker-Compose项目是Docker官方的开源项目&#xff0c;负责实现对Docker容器集群的快速编排。 Docker-Compose将所管理的容器分为三层&#xff0c;分别是 工程&#xff08;project&#xff09;&#xff0c;服务&#xff08;service&#xff09;以及容器&#xff08;cont…

【论文阅读 09】融合门控自注意力机制的生成对抗网络视频异常检测

2021年 中国图象图形学报 摘 要 背景&#xff1a; 视频异常行为检测是智能监控技术的研究重点&#xff0c;广泛应用于社会安防领域。当前的挑战之一是如何提高异常检测的准确性&#xff0c;这需要有效地建模视频数据的空间维度和时间维度信息。生成对抗网络&#xff08;GANs&…

Stable Diffusion 系统教程 | 强大的ControlNet 控制网

2023年的2月13日&#xff0c;一款名叫ControlNet的插件横空出世&#xff0c;AI绘画变得更加可控 ControlNet直译过来很简单&#xff0c;就叫做控制网&#xff0c;开发者是一名华裔&#xff0c;毕业于苏州大学&#xff0c;目前在斯坦福做读博士一年级&#xff0c;大佬大佬&…

【lesson10】fork创建进程的现象解答

文章目录 fork现象fork问题 fork现象 我们先来看一段代码。 大家觉得这段代码的printf会打印几次&#xff1f; 结果&#xff1a; 我们可以清楚的看到&#xff0c;第二个printf打印了2次。 我们再来看一段不可思议的代码&#xff1a; 运行结果&#xff1a; 我们可以看到这r…

批量、在线学习, 参数、非参数学习

批量学习&#xff08;Batch Learning&#xff09;和在线学习&#xff08;Online Learning&#xff09; 批量学习 批量学习的概念非常容易理解&#xff0c;我们之前介绍的许多机器学习算法&#xff0c;如果没有特殊说明&#xff0c;都可以采用批量学习的方式。批量学习的过程通…

iOS应用中的内存泄漏问题解决

解决iOS应用中的内存泄漏问题是非常重要的&#xff0c;因为内存泄漏可能导致应用变得缓慢或不稳定。以下是一些解决iOS内存泄漏问题的工具和方法&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 工具&…

SQLAlchemy Oracle Database 23c Free 集成之旅

SQLAlchemy & Oracle Database 23c Free 集成之旅 1. SQLAlchemy 是什么2. Oracle Database 23c Free 是什么3. 运行 Oracle Database 23c Free4. 学习 SQLAlchemy 统一教程4-1. 安装依赖库4-2. 建立连接 - 引擎4-3. 使用事务和 DBAPI4-3-1. 获取连接4-3-2. 提交更改4-3-3.…

【PowerQuery】Python自动刷新本地数据

Python数据刷新是开发爱好者和开发人员开发的PowerBI刷新模块进行数据刷新的手段,Python进行数据刷新是通过刷新PowerBI Desktop 的模式进行数据刷新。目前常用的Python的数据刷新模块是PbixRefresher,图为相关的模块和版本。 由于当前的脚本基于英文版本的PowerBI Desktop进…

【100天精通Python】Day69:Python可视化_实战:导航定位中预测轨迹和实际轨迹的3D动画,示例+代码

目录 1. 预测的3D轨迹和实际轨迹的动画图&#xff0c;同时动态更新 2 真值轨迹设置为静态的&#xff0c;预测轨迹不断更新 3 网格的三维坐标系有旋转运动&#xff0c;以此全方位展示预测轨迹和真值轨迹之间的空间关系 1. 预测的3D轨迹和实际轨迹的动画图&#xff0c;同时动态更…

vivo面试-Java

一、JAVA八股 1、Java实现线程的三种方式 (1) 继承 Thread 类&#xff1a; 创建一个新类&#xff0c;该类继承自Thread类&#xff0c;并重写run方法。然后创建该类的实例&#xff0c;并调用它的start方法来启动线程。 public class MyThread extends Thread {public void r…

【owt】 Intel® Media SDK for Windows: MSDK2021R1

https://www.intel.com/content/www/us/en/developer/articles/tool/media-sdk.html官方网不提供下载了: 2021地址 直接下载: MSDK2021R1.exe老版本 Intel Media SDK(Windows版本) 大神的介绍:owt-client-native 需要 https://github.com/open-webrtc-toolkit/owt-client…

罗德里格斯公式

1.点乘 A ⃗ ⋅ B ⃗ ∣ A ⃗ ∣ ∣ B ⃗ ∣ c o s ⟨ A ⃗ , B ⃗ ⟩ \vec{A} \cdot \vec{B} \left | \vec{A} \right | \left | \vec{B} \right | cos\left \langle \vec{A}, \vec{B} \right \rangle A ⋅B ​A ​ ​B ​cos⟨A ,B ⟩ 对应几何意义&#xff1a;向量 A ⃗…

面向使用者的git与gerrit相关笔记

git与gerrit相关笔记 前言一、gerrit是什么&#xff1f;二、一些配置1.先配置全局email 和name2.gerrit配置ssh key3.可能遇到的问题 三、提交代码和合并冲突常用Git命令三件套严格的要求 总结 前言 本文是介绍什么是gerrit和工作中git与gerrit相关的命令来避免一些提交代码的…

05_CSS进阶技巧

1 CSS 规范 遵循以下顺序 布局定位属性&#xff1a;display/position/float/clear/visibility/overflow&#xff08;建议 display 第一个写&#xff09;自身属性&#xff1a;width/height/margin/padding/border/background文本属性&#xff1a;color/font/text-decoraction/…

EMQX Enterprise 5.2 发布:Flow 设计器,Amazon Kinesis,Azure Event Hubs

EMQX Enterprise 5.2.0 版本现已正式发布&#xff01; 新版本带来了一系列重磅更新&#xff0c;最令人瞩目的是可拖拽的可视化 Flow 设计器&#xff0c;它可以帮助企业快速创建、测试和部署数据集成。同时&#xff0c;我们新增了对 Amazon Kinesis 和 Azure Event Hubs 的支持…