这份代码利用下面所有知识编写了一个简易聊天室(基于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结构体指针做为参数;
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
库中,使用 stdin
、stdout
、stderr
来表示标准输入、标准输出和标准错误,它们都是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