【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.rhkb.cn/news/134639.html

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

相关文章

Java入坑之语法糖

一、for和for-each 1.1for和for-each概念 for 循环是一种常用的循环结构&#xff0c;它可以通过一个变量&#xff08;通常是 i&#xff09;来控制循环的次数和范围。for 循环的语法格式如下&#xff1a; for (初始化; 布尔表达式; 更新) {//代码语句 }for-each 循环是 Java …

C语言指针详解(4)———找工作必看指针笔试题汇总

指针对于编程工作的重要性 C语言指针在找工作中具有重要性。以下是几个原因&#xff1a; 1.高效的内存管理&#xff1a;C语言指针可以帮助程序员高效地管理内存&#xff0c;包括动态内存分配和释放&#xff0c;以及数据的访问和操作。这对于开发性能优化的应用程序非常重要&am…

操作系统学习笔记---计算机系统概述

目录 概念 功能和目标 特征 并发 共享&#xff08;资源共享&#xff09; 虚拟 异步 发展与分类 手工操作阶段&#xff08;无OS&#xff09; 批处理阶段 单道批处理系统 多道批处理系统 分时操作系统 实时操作系统 网络操作系统 分布式计算机系统 个人计算机操…

Ubuntu安装Android Studio

一、Android Studio安装 官方教程&#xff1a;安装 Android Studio | Android Developers 1、下载&#xff1a;Download Android Studio & App Tools - Android Developers&#xff0c;选择linux版本 2、 提取/解压 将下载的安装包提取出来 3、 64位ubuntu系统&#…

Bash脚本学习:AWK, SED

1. AWK AWK 是一种编程语言&#xff0c;设计用于处理文件或数据流中基于文本的数据&#xff0c;或者使用 shell 管道。 可以将 awk 与 shell 脚本结合使用或直接在 shell 提示符下使用。 以上展示使用AWK分别打印第一个位置变量和第二个位置变量。 建立一个文档 csvtest.cs…

无涯教程-JavaScript - MATCH函数

描述 MATCH函数在单元格范围内搜索指定的项目,然后返回该项目在该范围内的相对位置。 当您需要某个项目在范围中的位置而不是项目本身时,请使用MATCH而不是LOOKUP函数之一。如。您可以使用MATCH函数为INDEX函数的row_num参数提供一个值。 语法 MATCH (lookup_value, lookup…

【数据结构】红黑树的删除(抽丝剥茧,带你理清每一种情况)

文章目录 前言正文1.所删除的结点为红色1.1delnode的左右都为空1.2delnode的左为空&#xff0c;且右不为空1.3delnode的左不为空&#xff0c;右为空1.4delnode的左不为空&#xff0c;且右不为空 2.所删除的结点为黑色2.1 调整后所在树每条路径黑色结点的个数不发生变化2.1 左结…

【问题处理】GIT合并解决冲突后,导致其他人代码遗失的排查

GIT合并解决冲突后&#xff0c;导致其他人代码遗失的排查 项目场景问题描述分析与处理&#xff1a;1. 警告分析2. 文件分析3. 问题关键4. 验证 解决策略总结 &#x1f4d5;作者简介&#xff1a;战斧&#xff0c;从事金融IT行业&#xff0c;有着多年一线开发、架构经验&#xff…

ChatGPT追祖寻宗:GPT-2论文要点解读

论文地址&#xff1a;Language Models are Unsupervised Multitask Learners 上篇&#xff1a;GPT-1论文要点解读 在上篇&#xff1a;GPT-1论文要点解读中我们介绍了GPT1论文中的相关要点内容&#xff0c;其实自GPT模型诞生以来&#xff0c;其核心模型架构基本没有太大的改变&a…

华为云云服务器云耀L实例评测 | 在华为云耀L实例上搭建电商店铺管理系统:一次场景体验

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

java写一个用于生成雪花id的工具类

我们创建一个类 叫 SnowflakeIdGenerator 作为生成雪花id的工具类 然后 编写代码如下 public class SnowflakeIdGenerator {private static final long START_TIMESTAMP 1609459200000L; // 设置起始时间戳&#xff0c;可以根据需要进行调整private static final long WORKER…

HBase 记录

HBase 管理命令 hbase hbck -details TABLE_NAME hbase hbck -repair TABLE_NAMEHBase概览 Master、RegionServer作用 RegionServer与Region关系 数据定位原理 https://blogs.apache.org/hbase/entry/hbase_who_needs_a_master RegionServer HBase Essentials.pdf (P25)…

四种常用的自动化测试框架

一直想仔细研究框架&#xff0c;写个流水账似的测试程序不难&#xff0c;写个低维护成本的测试框架就很难了&#xff0c;所以研究多种测试框架还是很有必要的&#xff0c;知道孰优孰劣&#xff0c;才能在开始编写框架的时候打好基础&#xff0c;今天读到了KiKi Zhao的翻译文章&…

中标麒麟--国产操作系统-九五小庞

那么&#xff0c;我国国产操作系统现状到底如何呢&#xff1f; 自 1999 年徐冠华部长一语点破我们的产业软肋之后&#xff0c;国产操作系统起步于国家“七五”计划期间&#xff0c;目前国产操作系统均是基于Linux内核进行的二次开发&#xff0c;中国国产操作系统进入Linux元年…

【网络】计算机网络基础

Linux网络 对网络的理解 在网络传输中存在的问题&#xff1a; 找到我们所需要传输的主机解决远距离数据传输丢失的问题怎么进行数据转发&#xff0c;路径选择的问题 有问题&#xff0c;就有解决方案&#xff1b; 我们把相同性质的问题放在一起&#xff0c;做出解决方案 解…

【Markdown】图片缩放

▚ 01 原图表示 语法为&#xff1a; ![替代文本](图片链接地址)其中&#xff0c;替代文本是在无法显示图片时显示的替代文本&#xff0c;而图片链接是指向图片的URL或相对路径。 例如&#xff0c;插入Panda图片&#xff1a; ![panda](https://img-blog.csdnimg.cn/e5f3…

(高频面试1)Redis缓存穿透、缓存击穿、缓存雪崩

目录 一&#xff1a;缓存数据 1.1 应用场景 1.2&#xff1a;缓存数据出现的问题 1.2.1 缓存穿透 1.2.2 解决办法 1.2.3 缓存击穿 1.2.4 解决办法 1.2.5 缓存雪崩 1.2.6 解决办法 一&#xff1a;缓存数据 1.1 应用场景 数据库查询结果缓存是一种常见的缓存应用场景&a…

MybatisPlus(5)

前言&#x1f36d; ❤️❤️❤️SSM专栏更新中&#xff0c;各位大佬觉得写得不错&#xff0c;支持一下&#xff0c;感谢了&#xff01;❤️❤️❤️ Spring Spring MVC MyBatis_冷兮雪的博客-CSDN博客 上篇讲了增删的操作&#xff0c;这篇讲修改操作中的一个问题以及它对应的…

DataX实现Mysql与ElasticSearch(ES)数据同步

文章目录 一、Linux环境要求二、准备工作2.1 Linux安装jdk2.2 linux安装python2.3 下载DataX&#xff1a; 三、DataX压缩包导入&#xff0c;解压缩四、编写同步Job五、执行Job六、定时更新6.1 创建定时任务6.2 提交定时任务6.3 查看定时任务 七、增量更新思路 一、Linux环境要求…

加密算法发展简介

1&#xff1a;对称加密算法 客户端加密数据和服务端解密数据&#xff0c;使用的相同的秘钥&#xff1a; 固定秘钥&#xff1a;双方约定好一个固定秘钥&#xff1b; 随机秘钥&#xff1a;双方约定每次建立连接的时候&#xff0c;某固定BYTE为秘钥&#xff1b; 缺点&#xff1a…