【Linux网络编程】Socket-UDP实例

这份代码利用下面所有知识编写了一个简易聊天室(基于Linux操作系统)。虽然字数挺多其实并不复杂,这里如果能够看完或许会对你的知识进行一下串联,这篇文章比较杂并且网络编程这块知识需要用到系统编程的知识,希望能帮助到您。

知识汇总:

1.IP地址与端口号

我们知道同一台主机的进程间通信有system V共享内存,消息队列,信号量这些方式,而跨主机的进程间通信怎么搞呢?使用IP地址与端口号!

IP地址用来网络中标识唯一一台主机,是一个32位无符号整数,常常用192.163.1.1这样点分十进制的字符串形式表示。

端口号用来表示一台主机中的一个进程,它是一个16位无符号整数,所以端口号最小是0,最大是65536。那么端口号如何表示一个进程呢?如下图​​​​,端口号作为数组的下标,数组中存放的是进程PID。它相当于一个哈希表,根据下标即端口号就可以找到对应的进程。

这里有一个问题,为什么不直接用进程PID呢,非要多走一步端口号,感觉有点多此一举。

我是这样理解的,我们使用的应用程序都是有对应的服务器维护的,我们作为一个客户端需要和服务器进行数据交互,那么就必须明白两个问题,一是服务器在哪,二是与服务器的哪个进程进行通信。当我们通信之前,就必须知道服务器的IP地址与进程PID,那么我们怎么知道呢?IP地址我们可以视为客户端提前知晓且并不变更,那进程PID呢?服务器每重新打开一次进程,PID会一样吗?显然不会,那么我怎么找到服务器的对应进程呢?这里就陷入了一个死循环。

网络:请问您是要和服务器123.123.123.123通信吗?

客户端:对的。

网络:请告诉我你是要和服务器的哪个进程通信呢?

客户端:不知道啊?它的进程每次重新启动,进程号都会变更。

网络:对不起先生,没有进程号我们没法帮您通信。

客户端:我不跟服务器通信我怎么知道服务器的进程号。

当然只有ip地址也是可以接收到数据的,但是交由哪个进程处理,这些数据是什么意思用来干什么的,就成了问题。

为了避免这个问题,就有了端口号的概念。服务器的相应进程会放到一个固定的端口号上,客户端都是提前知晓这个端口号的,所以在通信时,客户端只需要端口号就可以找到对应进程。这也使得许多端口号约定成俗,比如常见的8080端口。

2.主机序列与网络序列

每台计算机的存储顺序不同,分为大端存储和小端存储。大端存储就是低字节放到高地址,小端存储就是高字节放到低地址。如下图,定义一个int num=1;

可以看到01放到高地址处的是大端存储,放到低地址处的是小端存储。 

既然有这种主机存储顺序的不同,那么在进行网络通信时如果两个终端存储顺序不同,那么数据就会被错误解读。为了解决这个问题,就定义了一个共同的标准,在传输网络数据的时候都以大端存储为标准。

 因为客户端发送数据,携带的目的ip与目的端口都是网络序列的,服务器端要对比数据是给哪个端口,所以本地ip和端口必须转为网络序列。

3.多网卡/多IP

这块是关于创建套接字后,使用bind函数绑定端口号与ip的一个细节。

云服务器,或者一款服务器不要bind一个具体的ip,因为服务器可能有多个网卡多个ip地址,这些ip都有可能接收指定端口的数据,所以需要在服务器启动的时候bind任意一个ip地址,这就要求在对sockaddr里面的sin_addr里面的s_addr初始化时,使用INADDR_ANY进行初始化。

接口函数:

socket:

#include <sys/types.h>         
#include <sys/socket.h>int socket(int domain, int type, int protocol);

socket函数用来创建一个套接字。domain选择协议家族来进行通信,ipv4网络通信使用AF_INET,ipv6使用AF_INET6。type是用来选择套接字类型的,  SOCK_STREAM就是面向连接,可靠的,SOCK_DGRAM就是无连接,不可靠的。protocol用0即可,选择默认合适的协议。socket创建成功会返回一个文件描述符,创建失败返回-1。 

bind:

#include <sys/types.h>         
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

bind函数用来绑定本地主机ip与端口号。sockfd就是创建套接字成功返回的文件描述符。

我们可以看到sockaddr是个结构体,那这个结构体的成员有哪些呢?

sockaddr结构体:

__SOCKADDR_COMMON (sa_)就是#define  __SOCKADDR_COMMON(sa_prefix) \

  sa_family_t sa_prefix##family,其实绕来绕去就是sa_family_t  sa_family,一个16位短整型变量(下面sockaddr_in结构体的第一个成员也大体一样,sa_family_t  sin_family,一个16位短整型变量),用来表示地址类型如AF_INET。char sa_data[14]就是14字节的地址数据。

不过我们在进行网络通信时,使用的是sockaddr_in类型的结构体

sockaddr_in结构体:

由上图可以看出sockaddr结构体里面的sin_port是一个16位无符号整数,in_addr结构体里面有唯一一个成员---32位无符号整数。他们分别代表一个端口号和IP地址。sin_zero结构体就是填充字段,可以看到用sockaddr结构体大小减去了sockaddr_in结构体里面的三个成员的大小,最后自然sockaddr和sockaddr_in结构体的大小就一样了。这不明摆着是让sockaddr和sockaddr_in适配么。使用时直接取地址然后强转就可以了。

所以得出下面的结论:

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结构体指针做为参数;

addrlen就是一个无符号整形,指明sockaddr结构体大小的。

recvfrom:

#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);

利用创建的套接字把接收到的最大为len字节长度的数据放到buf中,flags标志位表示是否阻塞接收(设为0即可),src_addr指针和addrlen指针分别指向一个输入性参数,用来接收发送方的IP地址端口号以及结构体大小。数据成功则返回实际接收到的字符数,失败返回-1。

sendto:

#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);

利用创建的套接字发送最大为len字节长度的数据,flags标志位表示是否阻塞发送(设为0即可),dest_addr指针指向一个sockaddr_in结构体(里面有目的ip和目的端口号),addrlen为该结构体大小。成功则返回实际传送出去的字符数,失败返回-1。

inet_addr:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>in_addr_t inet_addr(const char *cp);

inet_addr() 函数将互联网主机地址 cp 字符串从 IPv4 数字和点表示法转换为按网络字节顺序的二进制数据。 

inet_ntoa:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);

inet_ntoa() 函数将按网络字节顺序给出的互联网主机地址转换为 IPv4 点分十进制表示法的字符串。 字符串以静态分配的缓冲区,后续调用将覆盖该缓冲区。不过这里的in_addr是sockaddr_in结构体里面的一个结构体成员,这个in_addr结构体里面存放的是一个32位无符号整数(IP地址)。

htons:

#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);

已知端口号是16位无符号整数,ip地址是32位无符号整数。所以这里四个函数就是把主机字节序转换成网络字节序or网络字节序转换成主机字节序,IP地址用uint32_t,端口号用uint16_t。

popen:

 #include <stdio.h>FILE *popen(const char *command, const char *type);int pclose(FILE *stream);

command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c标志,shell 将执行这个命令。
type: 只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。 

如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL。

man手册中关于popen函数的解释:popen() 函数通过创建管道、分叉和调用 shell 来打开进程。 由于管道根据定义是单向的,因此类型参数可以指定只有阅读或写作,而不是两者兼而有之;生成的流相应地是只读或只写的。

popen() 的返回值在所有方面都是正常的标准 I/O 流,除了它必须使用 pclose() 而不是 fclose(3) 关闭。 写入这样的流写入命令的标准输入;该命令的标准输出与调用 popen() 的进程的标准输出相同,除非命令对此进行了更改本身。相反,从“打开的”流中读取会读取命令的标准输出,并且命令的标准输入与进程的标准输入相同称为 popen()。

fopen函数:

可以看到popen函数与fopen函数极其相似,都是标准I/O库函数,且返回值都是一个文件流指针(FILE*),都需要用close函数关闭。但是fopen函数是用于打开一个文件,而popen函数作用是创建管道并创建子进程,并利用子进程处理command命令,处理结果返回到一个文件。调用popen函数的进程就是父进程。

fgets:文件I/O与标准I/O

 #include <stdio.h>char *fgets(char *s, int size, FILE *stream);

fgets() 从流中最多读取一个小于size大小的字符,并将它们存储到 S 指向的缓冲区中。 读取在 EOF 或换行符后停止。 如果是新的行被读取,它被存储到缓冲区中。 终止空字节 ('0') 存储在缓冲区中最后一个字符之后。

s 代表要保存到的内存空间的首地址,可以是字符数组名,也可以是指向字符数组的字符指针变量名。size 代表的是读取字符串的长度。stream 表示从何种流中读取,可以是标准输入流 stdin,也可以是文件流,即从某个文件中读取。

可以看到fgets函数与gets函数相似,但fgets函数更为安全,并且可以从文件中读取字符,而gets()只能从标准输入中获取。fegts还能检查预留存储区的大小,保证字符串不会超出预留空间。gets() 将一行从 stdin 读取到 s 指向的缓冲区中,直到终止换行符或 EOF,它用空字节 ('0') 替换它,但并不检查缓冲区是否溢出。

文件I/O与标准I/O部分:

写到这里有一个小问题,为什么stdin可以传入FILE*类型参数,stdin是什么?明白的可以自动跳过这里 。

下面主要是文件I/O与标准I/O的知识。。。。

我们知道当打开一个文件时,OS会先使用inode编号在磁盘文件系统里面去寻找这个文件,找到以后根据文件的属性为其创建一个内核层面的结构体来描述这个文件,该结构体里面含有文件的属性信息(大小,拥有者,创建修改时间)。当我们上层用户要对文件进行操作时,一定是需要使用系统调用函数(open,write,,read,close等等)依赖操作系统来进行操作,这些函数是底层用于文件I/O的)。为了提供比底层系统调用更为方便、好用的调用接口,设计了标准I/O库函数(fopen,fwrite,fread,fclose,fflush等等),使用时需要包含头文件<stdio.h>。

 对于标准 I/O 库函数来说,它们的操作是围绕 FILE 指针进行的,当使用标准 I/O 库函数打开或创建一个文件时,会返回一个指向 FILE 类型对象的指针,使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作(使用标准 I/O 库函数进行 I/O 操作),所以由此可知,FILE 指针的作用相当于文件描述符,只不过 FILE 指针用于标准 I/O 库函数中、而文件描述符则用于文件 I/O 系统调用中。

​ FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际 I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。FILE 数据结构定义在标准 I/O 库函数头文件 <stdio.h> 中。

FILE结构体如下图

通过上面两图可以看到,stdin其实就是一个结构体FILE* 指针。

在操作系统层面,当一个进程被启动时,进程会默认打开0,1,2号文件描述符对应标准输入设备文件,标准输出设备文件,标准错误设备文件,这些设备也相当于文件。

在用户(开发者)层面,标准 I/O 库中,使用 stdinstdoutstderr 来表示标准输入、标准输出和标准错误,它们都是FILE结构体指针,都有一个文件描述符,所以我们才可以通过库函数调用系统函数来对文件进行操作。当我们使用fopen函数打开一个文件时,返回函数就是FILE*类型指针,因为在标准I/O层面,无法使用文件描述符进行文件操作。



 

代码:

简介:下面的代码包括一个封装好的环形队列作为服务器接受客户端发送消息的容器、只需要传入互斥量指针就自动加锁自动解锁的类、封装好的线程类以及客户端服务器主程序。其实代码逻辑很简单,从udp_server.hpp的UdpServer类里面的私有成员变量入手就好。

服务器启动需要绑定一个端口号,端口号以命令行参数形式传入。

客户端启动需要在命令行输入服务器ip与端口号

udp_client.cc

#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/socket.h>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include <pthread.h>//sockaddr_in结构体的头文件,当然也包含一些主机转网络序列的函数比如htons
#include <netinet/in.h>
#include <arpa/inet.h>#include "error.hpp"static void* rfo(void *args)
{int sock=*(static_cast<int*>(args));while(true){//收char buffer[4096];struct sockaddr_in tmp;//输入型参数;socklen_t len=sizeof(tmp);//要初始化,不然没法修改;//阻塞式接收int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&tmp,&len);if(n>0)//接收服务器数据成功{buffer[n]=0;cout<<buffer<<endl;}}}//当传入程序参数个数不对时,调用这个Usage函数告诉他什么是他妈的惊喜!
static void Usage(string proc)
{cout<<"Usage:\n\t"<<proc<<" serverip "<<" serverport\n"<<endl;
}// ./udp_client serverip serverport
int main(int argc,char* argv[])
{if(argc!=3){Usage(argv[0]);exit(USAGE_ERR);}//保留输入的服务器的IP地址与端口号string serverip=argv[1];uint16_t serverport=atoi(argv[2]);int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){cerr << " create socket error " << strerror(errno) << endl;exit(SOCKET_ERR);}//client要不要bind呢?要的!socket通信的本质[clientip,clientport ::serverip,serverport]//要不要自己bind呢?不需要自己bind,也不要自己bind,OS自动bind--  客户端的端口号要操作系统随机分配,防止客户端出现启动冲突。//创建线程去接收;pthread_t tid;pthread_create(&tid,nullptr,rfo,(void*)&sock);//明确server是谁struct sockaddr_in server;memset((void*)&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(serverport);//主机序列转网络序列server.sin_addr.s_addr=inet_addr(serverip.c_str());//点分十进制字符串ip转成32位无符号整数并转为网络序列,这个函数有两个功能;while(true){string message;cout<<"please Enter# ";getline(cin,message);//在首次调用sendto函数时,操作系统自动给本程序绑定IP地址和端口号,客户端不能自己绑定端口号和ip地址,因为端口号和IP地址会变。sendto(sock,message.c_str(),message.size(),0,(const struct sockaddr*)&server,sizeof(server));}return 0;
}

udp_server.cc


#include <iostream>
#include "udp_server.hpp"#include <memory>
#include <cstdio>using namespace ns_server;// 上层的业务处理,不关心网络发送,只负责信息处理即可
// 客户端输入命令,服务器执行命令,结果返回给客户端;// 业务1(字符串全部转大写)
string transaction(string request)
{string result;char c;for (auto &e : request){if (islower(e)){c = toupper(e);result.push_back(c);}else{result.push_back(e);}}return result;
}bool notsecure(string &command)
{bool ret = false;int pos;pos = command.find("rm");if (pos != string::npos)ret = true;pos = command.find("while");if (pos != string::npos)ret = true;pos = command.find("mv");if (pos != string::npos)ret = true;pos = command.find("kill");if (pos != string::npos)ret = true;return ret;
}
// 业务二(服务器端获取命令字符串,服务器执行完成后给客户端返回结果)
string excuteCommand(string command)
{// 1.安全检查if (notsecure(command))return "Sorry,you can do that!";// 2.业务逻辑处理FILE *fp = popen(command.c_str(), "r");//popen函数是创建管道在创建子进程,利用子进程来处理命令,并把结果输出到一个文件的,返回值是文件指针。if (fp == nullptr)return "None";// 3.获取结果char line[1024];string result;// 这里用while的原因是fgets函数遇到换行符或EOF读取结束,也就是说一次读一行,使用while循环读到文件结尾;while (fgets(line, sizeof(line), fp)!=nullptr){result += line;}pclose(fp);return result;
}// 当传入程序参数个数不对时,调用这个Usage函数告诉他什么是他妈的惊喜!
static void Usage(string proc)
{cout << "Usage:\n\t" << proc << " port\n"<< endl;
}// ./udp_server     serverport(服务器自己设置端口号)
int main(int argc, char *argv[])
{if (argc != 2) // 命令行传入参数不够{Usage(argv[0]);exit(USAGE_ERR);}// 把字符串port转换成16位整数uint16_t port = atoi(argv[1]);// 智能指针构造UdpServer对象,构造函数需要传入自己想定义的port//unique_ptr<UdpServer> usvr(new UdpServer(excuteCommand, port));unique_ptr<UdpServer> usvr(new UdpServer(port));//usvr->InitServer();  // 服务器初始化usvr->StartServer(); // 服务器开始服务return 0;
}

udp_server.hpp

#pragma once
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <string>
#include <pthread.h>
#include <stdlib.h>
#include <functional>
#include <unordered_map>
// sockaddr_in结构体的头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include "error.hpp"
using namespace std;
#include "RingQueue.hpp"
#include "Thread.hpp"
#include "lockGuard.hpp"namespace ns_server
{// const uint16_t default_port = 8081;using func_t = function<string(string)>; // func_t是指代返回值为string,参数为string的函数指针;class UdpServer{public:// // 构造服务器对象必须绑定端口号,指定服务器处理方法// UdpServer(func_t cb, uint16_t port = default_port)//     : _port(port), _service(cb)// {//     cout << " Server Port : " << _port << endl;// }// 构造服务器对象必须绑定端口号,指定服务器处理方法UdpServer(uint16_t port): _port(port), _p(){cout << " Server Port : " << _port << endl;pthread_mutex_init(&_mutex, nullptr); // 初始化锁;// 这里使用c++11 bind函数,相当于函数适配器,构建了一个可调用对象,函数参数顺序也可以占位符标定,_1,_2类似这样;_p = new Thread(1, bind(&UdpServer::Recv, this));_c = new Thread(1, bind(&UdpServer::Broadcast, this));}void StartServer(){// 1.创建socket接口,打开网络文件;_socket = socket(AF_INET, SOCK_DGRAM, 0);if (_socket < 0){cerr << " create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}cout << " create socket success: " << _socket << endl; // 3// 2.给服务器绑定本地IP和端口号(要知道是哪个IP哪个端口号接收数据)struct sockaddr_in local;bzero(&local, sizeof(local)); // 清零local.sin_family = AF_INET;local.sin_port = htons(_port);             // 端口号local.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址if (bind(_socket, (const struct sockaddr *)&local, sizeof(local)) < 0) // 绑定本地Ip与端口号{cerr << " bind socket error: " << strerror(errno) << endl;exit(BIND_ERR);}cout << " bind socket success: " << _socket << endl;_p->run();_c->run();}void addUser(const string &name, const struct sockaddr_in &peer){lockGuard lock(&_mutex);auto it = _onlineUser.find(name);if (it != _onlineUser.end())return;// 没有就插入_onlineUser.insert(pair<string, struct sockaddr_in>(name, peer));}// 接收client数据并记录用户ip和端口void Recv(){char buffer[1024];while (true){// 收struct sockaddr_in peer; // 输入性参数,获得客户端ip与端口号socklen_t len = sizeof(peer);int n = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 接收客户端发送过来的消息和客户端ip与端口if (n > 0)buffer[n] = '\0';elsecontinue;// 提取client信息---debug;string client_ip = inet_ntoa(peer.sin_addr);uint16_t client_port = ntohs(peer.sin_port);                        // 网络序列转为主机序列cout << client_ip << "-" << client_port << " # " << buffer << endl; // 显示客户端发来的数据// 利用ip和端口构建一个用户名string name = client_ip;name += "-";name += to_string(client_port);addUser(name, peer); // 存入用户ip和端口,后面把消息转发给所有用户string message=name;message+=">>";message+=buffer;_rq.push(message);    // 接收到的消息加工一下存入环形队列;// 业务处理// string message = _service(buffer);// 发// sendto(_socket, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));}}// 广播void Broadcast(){while (true){string message;// 因为封装好的环形队列里面有信号量和互斥量,所以这里不必担心线程安全问题;_rq.pop(&message);vector<struct sockaddr_in> v;// 这里需要设置互斥量,因为两个线程访问了临界资源,结果具有不确定性;{lockGuard lock(&_mutex);for (auto &user : _onlineUser){v.push_back(user.second);}}for (auto &e : v) // 给所有用户发消息;{sendto(_socket, message.c_str(), message.size(), 0, (const struct sockaddr *)&e, sizeof(e));// 测试消息发送出去了没cout << "send done..." << message << endl;}}}~UdpServer(){pthread_mutex_destroy(&_mutex);// 等待线程结束_p->join();_c->join();// 回收堆空间;delete _p;delete _c;}private:int _socket;uint16_t _port;// func_t _service; // 上一个版本只是简单的IO,现在要进行业务处理;unordered_map<string, struct sockaddr_in> _onlineUser; // 把所有用户ip和端口保存起来,后面要给所有人转发消息;RingQueue<string> _rq;                                 // 环形队列存放用户发的消息;pthread_mutex_t _mutex;// 两个线程,一个收,一个发;Thread *_p;Thread *_c;};
}
#pragma onceenum{   USAGE_ERR=1,SOCKET_ERR,BIND_ERR};

RingQueue.hpp

#pragma once#include<iostream>
#include<semaphore.h>#include<ctime>
#include<unistd.h>
#include<vector>using namespace std;const int N=50;template<class T>
class RingQueue
{void P(sem_t* sem){sem_wait(sem);}void V(sem_t* sem){sem_post(sem);}void Lock(pthread_mutex_t& mutex){pthread_mutex_lock(&mutex);}void UnLock(pthread_mutex_t& mutex){pthread_mutex_unlock(&mutex);}
public:RingQueue(int num=N):_ring(num),_cup(num),_consumer_step(0),_productor_step(0){sem_init(&_data_sem,0,0);sem_init(&_space_sem,0,_cup);pthread_mutex_init(&_c_mutex,nullptr);pthread_mutex_init(&_p_mutex,nullptr);}void push(const T& in){P(&_space_sem);Lock(_p_mutex);_ring[_productor_step++]=in;_productor_step %= _cup;//消费者信号量加一;(数据)V(&_data_sem);UnLock(_p_mutex);}void pop(T* out){P(&_data_sem);Lock(_c_mutex);*out=_ring[_consumer_step++];_consumer_step %= _cup;//生产者信号量加一(空间)V(&_space_sem);UnLock(_c_mutex);}~RingQueue(){sem_destroy(&_data_sem);sem_destroy(&_space_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);}private:vector<T> _ring;//数组模拟环形队列int  _cup;//容量sem_t _data_sem;//消费者信号量sem_t _space_sem;//生产者信号量int _consumer_step;//消费者下标int _productor_step;//生产者下标// 单生产和单消费不存在竞争问题,只要有信号量即可;但是多生产和多消费的线程,可能都申请到了信号量,但是都在竞争同一块资源,无法保证原子性;pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex;};

lockGuard.hpp

#pragma once#include <pthread.h>
#include <iostream>class Mutex//成员:加锁函数和解锁函数
{
public:Mutex(pthread_mutex_t* pmutex):_pmutex(pmutex)   {}void lock(){pthread_mutex_lock(_pmutex);}void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}private:pthread_mutex_t* _pmutex;//需要传入一个互斥量(锁)的指针;
};//对Mutex进行二次封装;
//创建该对象时自动加锁,析构时自动解锁;
class lockGuard
{   
public:lockGuard(pthread_mutex_t* pmutex):_mutex(pmutex)//利用锁的指针构建Mutex对象{_mutex.lock();}~lockGuard(){_mutex.unlock();}private:Mutex _mutex;//类内创建对象
};

Thread.hpp

#pragma once#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdlib>
#include <string>class Thread
{
public://typedef void (*func_t) (void*);using func_t=function<void()>;//fun_t:无返回值,无参数的函数指针;typedef enum{NEW=0,RUNNING,EXITED}ThreadStatus;public:Thread(int num,func_t func):_tid(0),_status(NEW),_func(func){char name[128];snprintf(name,sizeof(name),"thread-%d",num);_name=name;}//状态:new,running,exitedint status(){return _status;}//线程名std::string threadname(){return _name;}//线程ID(共享库中的进程地址空间的虚拟地址)pthread_t threadid(){if(_status==RUNNING)//线程已经被创建,线程id已经输入到成员变量_tid中;return _tid;else {std::cout<<"thread is not running,no tid!"<<std::endl;return 0;}}static void* runHelper(void *args){//静态成员函数不能访问类内所有成员,因为没有this指针;Thread* td=(Thread*)args;(*td)();//该对象调用仿函数;return nullptr; }void operator()()//仿函数{_func();}//创建线程void run(){//因为runHelper函数必须只能有一个void*参数,所以runHelper函数在类内必须定义为static,这样才没有this指针;int n=pthread_create(&_tid,nullptr,runHelper,this);if(n!=0) return exit(0);//线程创建失败,那么直接退出进程;_status=RUNNING;}//等待线程结束void join(){int n=pthread_join(_tid,nullptr);if(n!=0) {std::cerr<<"main thread join thread "<<_name<<" error "<<std::endl;return;}_status=EXITED;//线程退出;}
private:pthread_t _tid;//线程ID(原生线程库中为该线程所创建的TCB起始虚拟地址)std::string _name;//线程名func_t _func;//线程要执行的回调//void* _args;//线程回调函数参数ThreadStatus _status;//枚举类型:状态};

makefile

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

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

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

相关文章

Spring Boot集成EasyExcel实现数据导出

在本文中&#xff0c;我们将探讨如何使用Spring Boot集成EasyExcel库来实现数据导出功能。我们将学习如何通过EasyExcel库生成Excel文件&#xff0c;并实现一些高级功能&#xff0c;如支持列下拉和自定义单元格样式&#xff0c;自适应列宽、行高&#xff0c;动态表头 &#xff…

Open3D 点云配准——可视化匹配点对之间的连线

点云配准 一、算法原理1、概述2、主要函数二、代码实现三、结果展示四、测试数据本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 1、概述 可视化源点云和目标点云中匹配点对之间的连线,这对于点云配准,尤…

【AI语言大模型】星火使用介绍

一、前言 现在AI语言大模型是百花齐放,挺好!有竞争,有发展,才能推出更好的产品。现在,科大讯飞就推出了大语言模型——星火!能够学习和理解人类的语言,进行多轮对话,回答问题,高效便捷地帮助人们获取信息、知识和灵感。星火在对话栏设置了三个插件:文档回答、PPT生成…

unity 使用声网(Agora)实现语音通话

第一步、先申请一个声网账号 [Agora官网链接]&#xff08;https://console.shengwang.cn/&#xff09; 第二步在官网创建项目 &#xff0c;选择无证书模式&#xff0c;证书模式需要tokenh和Appld才能通话 第三步 官网下载SDK 然后导入到unity&#xff0c;也可以直接在unity商店…

Linux界的老古董

Slackware 是由 Patrick Volkerding 制作的 Linux 发行版&#xff0c;从 1993 年发布至今也一直在 Patrick 带领下进行维护。7 月 17 日&#xff0c;Slackware 才刚刚过完它 24 岁的生日&#xff0c;看似年纪轻轻的它&#xff0c;已然是 Linux 最古老的发行版。 Slackware 的发…

第 363 场 LeetCode 周赛题解

A 计算 K 置位下标对应元素的和 模拟 class Solution { public:int pop_cnt(int x) {//求x的二进制表示中的1的位数int res 0;for (; x; x >> 1)if (x & 1)res;return res;}int sumIndicesWithKSetBits(vector<int> &nums, int k) {int res 0;for (int i…

Linux文件属性操作函数

1.access函数 #include <unistd.h> int access(const char *pathname, int mode); 作用&#xff1a;判断某个文件是否有某个权限&#xff0c;或者判断文件是否存在 参数: -pathname:判断的文件路径 -mode: R_OK&#xff1a;判断是否有读权限 W_OK X_OK F_OK&#xff1a;…

【海报生成器源码】设计海报生成器网站开源源码(更新)

源码简介: 随着社会经济和商业发展&#xff0c;对产品宣传的需求也加大了。如何快速制作海报也成了很大的需求。这里分享的是一个海报生成器网站的最新源代码。 这个海报编辑器有着实用强大的功能&#xff0c;它的最左侧是组件列表。可以在最左侧选择组件&#xff0c;比如文本…

华为云云耀云服务器L实例评测 | 开启OPC UA之旅

OPC Unified Architecture (OPC UA)是一种用于工业自动化的M2M协议(Machine-to-machine)&#xff0c;具有平台独立性&#xff0c;在Windows和Linux上都可以运行。随着云服务在工业现场的不断普及&#xff0c;OPCUA服务也开始大量部署在云端。 本文以华为云云耀云服务器L为基础…

3D目标检测数据集 KITTI(标签格式解析、点云转图像、点云转BEV)

本文介绍在3D目标检测中&#xff0c;理解和使用KITTI 数据集&#xff0c;包括KITTI 的基本情况、下载数据集、标签格式解析、点云转图像、点云转BEV。 目录 1、KITTI数据集中3D框可视化的效果 2、先看个视频&#xff0c;了解KITTI 的基本情况 3、来到KITTI官网&#xff0c;下…

计算机竞赛 机器视觉的试卷批改系统 - opencv python 视觉识别

文章目录 0 简介1 项目背景2 项目目的3 系统设计3.1 目标对象3.2 系统架构3.3 软件设计方案 4 图像预处理4.1 灰度二值化4.2 形态学处理4.3 算式提取4.4 倾斜校正4.5 字符分割 5 字符识别5.1 支持向量机原理5.2 基于SVM的字符识别5.3 SVM算法实现 6 算法测试7 系统实现8 最后 0…

Docker Swarm集群部署

Docker Swarm集群部署 任务平台 3台虚拟机&#xff0c;一台作为manager 节点&#xff0c;另两台作为work节点。 文章目录 Docker Swarm集群部署安装docker配置防火墙开放端口在 manager 节点创建 Swarm 集群创建用于swarm服务的自定义的overlay网络测试跨主机容器通信 安装do…

React使用useImperativeHandle实现父组件触发子组件事件

相关知识&#xff1a; useImperativeHandle forwardRef 相关代码&#xff1a; 获取子组件实例&#xff0c;由于这是函数组件&#xff0c;没有this因此不能整体获取&#xff0c;我们可以通过useImperativeHandle获取想要的变量或者方法。 父组件import React, { useRef } fro…

【华为云云耀云服务器L实例评测|云原生】自定制轻量化表单Docker快速部署云耀云服务器

&#x1f935;‍♂️ 个人主页: AI_magician &#x1f4e1;主页地址&#xff1a; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f468;‍&#x1f4bb;景愿&#xff1a;旨在于能和更多的热爱计算机的伙伴一起成长&#xff01;&#xff01;&…

CSS 浮动布局

浮动的设计初衷 float: left/right/both;浮动是网页布局最古老的方式。 浮动一开始并不是为了网页布局而设计&#xff0c;它的初衷是将一个元素拉到一侧&#xff0c;这样文档流就能够包围它。 常见的用途是文本环绕图片&#xff1a; 浮动元素会被移出正常文档流&#xff0c;…

【算法|链表】环形链表Ⅱ

环形链表Ⅱ 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统…

无涯教程-JavaScript - COS函数

描述 COS函数返回给定Angular的余弦值。 语法 COS (number)争论 Argument描述Required/OptionalNumber The angle in radians for which you want the cosine.Required Notes 如果Angular以度为单位,则将Angular乘以PI()/180或使用RADIANS函数将Angular转换为弧度 弧度(…

Python 魔法方法

视频版教程 Python3零基础7天入门实战视频教程 Python的魔法方法&#xff0c;也称为特殊方法或双下划线方法&#xff0c;是一种特殊的方法&#xff0c;用于在类中实现一些特殊的功能。这些方法的名称始终以双下划线开头和结尾&#xff0c;例如__init__&#xff0c;repr&#x…

uniapp实现大气质量指标图(app端小程序端均支持,app-nvue不支持画布)

效果图如下&#xff1a; 思路&#xff1a; 1.首先我想到的就是使用图标库echarts或ucharts&#xff0c;可是找了找没有找到类似的。 2.其次我就想用画布来实现这个效果&#xff0c;直接上手。&#xff08;app-vue和小程序均可以实现&#xff0c;但是在app-nvue页面不支持画布…

033:跨域,vue端和 Nignx反向代理的配置详细解析

第033个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…