文章目录
- 网络编程套接字(2): 简单的UDP网络程序
- 3. 简单的UDP网络程序
- 3.1 服务端创建
- (1) 创建套接字
- (2) 绑定端口号
- (3) sockaddr_in结构体
- (4) 数据的接收与发送
- 接收
- 发送
- 3.2 客户端创建
- 3.3 代码编写
- (1) v1_简单发送消息
- (2) v2_小写转大写
- (3) v3_模拟命令行解释器
- (4) v4_多线程版本的群聊系统
- (5) v5_Windows与Linux配合聊天室
网络编程套接字(2): 简单的UDP网络程序
3. 简单的UDP网络程序
3.1 服务端创建
(1) 创建套接字
create an endpoint for communication: 创建用于通信的端点
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int socket(int domain, int type, int protocol);
参数说明:
第一个参数domain: 指定套接字的通信域
第二个参数type: 指定套接字的服务类型(套接字的种类)
第三个参数protocol: 代表创建套接字的协议(默认为0), 给0,系统会自动判断是tcp还是udp
返回值:
套接字创建成功: 返回一个文件描述符
套接字创建失败: 返回-1, 并且设置错误码
关于socket参数详细介绍:
(1) domain: 指定套接字的通信域,相当于 struct sockaddr结构体的前16比特位(2字节)
domain的选项是以宏的形式给出的,我们直接选用即可。常用就是上面框住的两个:
- AF_UNIX,本地通信
- AF_INET(IPv4)或者 AF_INET6(IPv6),网络通信
(2) type: 指定套接字的服务类型
该参数的选项也是像domain一样以宏的形式给出,直接选用。常用的是上面两个:
-
SOCK_STREAM: 基于TCP的网络通信,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)
-
SOCK_DGRAM: 基于UDP的网络通信,套接字数据报,提供的用户数据报服务(对应UDP的特点:面向数据报)
(2) 绑定端口号
bind a name to socket:将名称绑定到套接字
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
第一个参数sockfd: 文件描述符, 即要绑定的套接字
第二个参数addr: 网络相关的结构体, 包含IP地址、端口号等
第三个参数addrlen: 传入结构体addr(第二个参数)的实际长度大小
返回值:
绑定成功: 返回0
绑定失败: 返回-1,并且设置错误码
参数addr的类型是:struct sockaddr *,也就是如图的结构体
我们需要做的就是:定义一个 sockaddr_in 的结构体,即上图的第二个结构体,然后对该结构体进行内容填充,填完就把给结构体传给第二个参数addr,需要强制类型转换
(3) sockaddr_in结构体
- __SOCKADDR_COMMON是一个宏
#define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family
sa_prefix:代表外面传入的参数sin_
sa_prefix##family:##代表合并拼接,意思是sa_prefix与family合并拼接
sa_prefix就是sin_,则sa_prefix##family表示sin_family
sa_family_t:代表16位整数
就是这16位地址类型
- sin_port: 是当前服务器需要绑定的端口号,它的类型是 in_port_t,代表16位的整数
- sin_addr: 代表IP地址,它的类型是一个in_addr的结构体,它里面的内容是32位的整数
-
关于这个IP地址:我们要传入字符串风格的,但是这里需要4字节整数风格,所以需要转化,比如"1.1.1.1"-> uint32_t,问:能不能强转呢?
不能强转, 强转只能改变类型, 不改变二进制构成 -
我们转化完了还是本主机的4字节序列,需要网络序列,所以要将主机序列转化成为网络序列
上面的2步用 inet_addr函数就可以完成
- 但是我们的云服务器,或者一款服务器,一般不要指明某一个确定的IP
所以这里的ip地址我们填 INADDR_ANY,这是一个宏,代表 0.0.0.0,叫做任意地址绑定
- sin_zero: 表示该结构体的填充字段(即上面讲的sin_family,sin_port,sin_addr.s_add)
总结: 未来使用这个函数时,需要所以填充:sin_family,sin_port,sin_addr.s_addr这3个字段,因为不关注其他字段,所以在填充之前需要对该结构体清空,我们可以采用 memset或 bzero函数来完成。
bind的作用:
上面如果我们只设置了sockaddr_in这个结构体,它只是在用户空间的特定函数栈帧上,不在内核中,所以bind的作用就是把文件字段进行绑定关联,这样这个文件就是网络文件
(4) 数据的接收与发送
接收
receive a message from a socket:从套接字接收消息
头文件:
#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);
参数说明:
第一个参数sockfd: 文件描述符,就是上面我们绑定好的套接字
第二个参数buf: 接收数据的缓冲区(自己定义)
第三参数len: 缓冲区的长度
第四个参数flags: 读取方式,默认设为0表示阻塞式读取
第五个参数src_addr: (输入)对应套接字的接收缓冲区
第六个参数addrlen:(输出)src_addr结构体的长度
返回值:
成功: 返回实际读到的字节数
失败: 失败返回-1,并设置错误码
socklen_t 是一个32位的无符号整数
-
参数src_addr与addrlen:输入输出型参数
-
src_addr: 输入时传入对应套接字的接收缓冲区,输出时包含客户端的ip和port
-
addrlen: 输入时传入对应套接字的接收缓冲区,输出时表示实际输出的结构体大小
我们做的是定义一个 sockaddr_in 的结构体,把结构体传给参数src_addr,需要强制类型转换
发送
send a message on a socket: 在套接字上发送消息
头文件:
#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);
参数说明:
第一个参数sockfd: 文件描述符,从哪个套接字去发送消息
第二个参数buf: 发送数据的缓冲区
第三参数len: 要发送的数据长度
第四个参数flags:发送方式,默认设为0表示阻塞式发送
第五个参数dest_addr:下面解释
第六个参数addrlen:dest_addr结构体的长度
返回值:
成功: 实际发送的字节数
失败: 失败返回-1,并设置错误码
-
dest_addr和addrlen 是一个输入型参数
-
dest_addr:指向目的地址的结构体指针,表示要发给谁
-
addrlen:表示目的地址结构体的长度
我们做的是定义一个 sockaddr_in 的结构体,然后对该结构体进行内容填充,填完就把给结构体传给dest_addr**,需要强制类型转换**
3.2 客户端创建
还是3步:创建套接字,bind(不需要自己绑定,由OS自动分配),处理数据接收与发送
3.3 代码编写
这里一共提供5个版本的udp代码
err.hpp
:这个代码是公用的后续不在给出
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,
BIND_ERR,
};
(1) v1_简单发送消息
客户端向服务端发送消息,服务端收到后再把消息发回给客户端
udp_server.hpp
#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include"err.hpp"
using namespace std;
namespace ns_server
{
const static uint16_t default_port=8080;
class Udpserver
{
public:
Udpserver(uint16_t port=default_port)
:port_(port)
{
cout<<"server addr: "<<port_<<endl;
}
void InitServer()
{
// 1. 创建socket接口, 打开网络文件(本质)
sock_=socket(AF_INET,SOCK_DGRAM,0);
if(sock_<0)
{
cerr<<"create socket error: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout<<"create socket success: "<<sock_<<endl; // sock_ = 3
// 2. 给服务器指明IP地址和Port端口号
// 填充一下服务器的IP和Port
struct sockaddr_in local; // 里面有很多字段 local是在用户空间的特定函数栈帧上,不在内核中!
// 清空local
bzero(&local,sizeof(local)); // 用memset也可以
// 填充sockaddr_in结构
local.sin_family=AF_INET;
local.sin_port=htons(port_); // 端口号要出现在网络中, 主机序列转网络序列
// 使用 inet_addr就可以做下面两件事情:
// (1) 字符串风格的IP地址,转换成为4字节int --- 不能强转, 强转只能改变类型, 不改变二进制构成
// (2) 需要将主机序列转化成为网络序列
// (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
local.sin_addr.s_addr=INADDR_ANY; // 让我们的udp_server在启动的时候, bind本主机上的任意IP
// 把套接字字段和文件字段进行关联 --- 网络文件
int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
cerr<<"bind socket error: "<<strerror(errno)<<endl;
exit(BIND_ERR);
}
cout<<"bind socket success: "<<sock_<<endl;
}
void Start()
{
char buffer[1024]; // 保存用户数据的缓冲区
while(true)
{
// 收到来自客户端发送的消息
struct sockaddr_in peer; // 远端
socklen_t len = sizeof(peer); // 这里一定要写清楚, 未来你传入的缓冲区大小
// 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer, &len);
if(n>0) // 读取数据成功
buffer[n]='\0';
else
continue;
// 提取client信息 --- debug
string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
uint16_t clientport=ntohs(peer.sin_port); // 网络序列转主机序列
cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;
// 把消息发给别人
// 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
// 谁给我发的, 我就把消息转给谁
// peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
sendto(sock_,buffer,strlen(buffer),0,(struct sockaddr*)&peer, sizeof(peer));
}
}
~Udpserver()
{}
private:
int sock_; //套接字(文件描述符)
uint16_t port_; //端口号(本地主机序列构建的port)
};
}
udp_server.cc
#include"udp_server.hpp"
using namespace ns_server;
// 运行格式: ./udp_server port
// 使用手册
static void usage(string proc)
{
cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}
int main(int argc,char*argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=atoi(argv[1]); //命令行参数转换成uint16_t类型
unique_ptr<Udpserver> usvr(new Udpserver(port));
usvr->InitServer(); // 服务器初始化
usvr->Start();
return 0;
}
udp_client.cc
#pragma once
#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试
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]);
// 1. 创建套接字
int sock=socket(AF_INET,SOCK_DGRAM,0);
if (sock < 0)
{
cerr << "create socket error: " << strerror(errno) << endl;
exit(SOCKET_ERR);
}
// 2. 关于客户端的绑定
// client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]
// 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
// client的port要随机让OS分配防止client出现启动冲突
// server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化
// 明确服务器是谁
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());
// 3. 向服务器发送消息(这里暂时由用户充当)
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)-1,0,(struct sockaddr*)&temp,&len);
if(n>0)
{
buffer[n]='\0';
cout<<"server echo# "<<buffer<<endl;
}
}
}
运行结果:
先运行服务端,再启动客户端,客户端先用本地环回进行测试,测试成功
运行程序后看到套接字是创建成功的,对应得到到的文件描述符是3,这也很好理解,因为0、1、2默认被标准输入流、标准输出流和标准错误流占用了,此时最小的、未被使用用的文件描述符就是3
(2) v2_小写转大写
v2在v1版本的基础增加了业务处理,上层使用了回调函数实现大小写转换
udp_server.hpp
#pragma once
#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
using namespace std;
namespace ns_server
{
const static uint16_t default_port=8080;
using func_t =function<string(string)>; //这是一个函数
class Udpserver
{
public:
Udpserver(func_t cb, uint16_t port=default_port)
:service_(cb)
,port_(port)
{
cout<<"server addr: "<<port_<<endl;
}
void InitServer()
{
// 1. 创建socket接口, 打开网络文件(本质)
sock_=socket(AF_INET,SOCK_DGRAM,0);
if(sock_<0)
{
cerr<<"create socket error: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout<<"create socket success: "<<sock_<<endl; // sock_ = 3
// 2. 给服务器指明IP地址和Port端口号
// 填充一下服务器的IP和Port
struct sockaddr_in local; // 里面有很多字段 local是在用户空间的特定函数栈帧上,不在内核中!
// 清空local
bzero(&local,sizeof(local)); // 用memset也可以
// 填充sockaddr_in结构
local.sin_family=AF_INET;
local.sin_port=htons(port_); // 端口号要出现在网络中, 主机序列转网络序列
// 使用 inet_addr就可以做下面两件事情:
// (1) 字符串风格的IP地址,转换成为4字节int --- 不能强转, 强转只能改变类型, 不改变二进制构成
// (2) 需要将主机序列转化成为网络序列
// (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
local.sin_addr.s_addr=INADDR_ANY; // 让我们的udp_server在启动的时候, bind本主机上的任意IP
// 把套接字字段和文件字段进行关联 --- 网络文件
int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
cerr<<"bind socket error: "<<strerror(errno)<<endl;
exit(BIND_ERR);
}
cout<<"bind socket success: "<<sock_<<endl;
}
void Start()
{
char buffer[1024]; // 保存用户数据的缓冲区
while(true)
{
// 收到来自客户端发送的消息
struct sockaddr_in peer; // 远端
socklen_t len = sizeof(peer); // 这里一定要写清楚, 未来你传入的缓冲区大小
// 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer, &len);
if(n>0) // 读取数据成功
buffer[n]='\0';
else
continue;
// 提取client信息 --- debug
string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
uint16_t clientport=ntohs(peer.sin_port); // 网络序列转主机序列
cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;
// 做业务处理
string response=service_(buffer);
// 把消息发给别人
// 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
// 谁给我发的, 我就把消息转给谁
// peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
sendto(sock_,response.c_str(),response.size(),0,(struct sockaddr*)&peer, sizeof(peer));
}
}
~Udpserver()
{}
private:
int sock_; //套接字(文件描述符)
uint16_t port_; //端口号(本地主机序列构建的port)
func_t service_; //我们的网络服务器刚刚解决的是网络IO的问题, 要进行业务处理(一个类内的回调方法)
};
}
udp_server.cc
#include"udp_server.hpp"
using namespace ns_server;
// 运行格式: ./udp_server port
// 使用手册
static void usage(string proc)
{
cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}
// 上层的业务处理, 不关心网络发送, 只负责信息处理即可
// 这里是小写转大写
string transactionString(string request) // request就是一个字符串
{
string ret;
char c;
for(auto&r:request)
{
if(islower(r))
{
c=toupper(r);
ret.push_back(c);
}
else
{
ret.push_back(r);
}
}
return ret;
}
int main(int argc,char*argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=atoi(argv[1]); //命令行参数转换成uint16_t类型
unique_ptr<Udpserver> usvr(new Udpserver(transactionString,port));
usvr->InitServer(); // 服务器初始化
usvr->Start();
return 0;
}
udp_client.cc
#pragma once
#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试
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]);
// 1. 创建套接字
int sock=socket(AF_INET,SOCK_DGRAM,0);
if (sock < 0)
{
cerr << "create socket error: " << strerror(errno) << endl;
exit(SOCKET_ERR);
}
// 2. 关于客户端的绑定
// client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]
// 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
// client的port要随机让OS分配防止client出现启动冲突
// server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化
// 明确服务器是谁
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());
// 3. 向服务器发送消息(这里暂时由用户充当)
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)-1,0,(struct sockaddr*)&temp,&len);
if(n>0)
{
buffer[n]='\0';
cout<<"server echo# "<<buffer<<endl;
}
}
}
运行结果:
(3) v3_模拟命令行解释器
v3是在v2原有的业务处理下修改了功能,只要我们在客户端输入命令服务端就会返回运行结果,popen函数可以实现简单的命令行解释
udp_server.hpp
#pragma once
#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
using namespace std;
namespace ns_server
{
const static uint16_t default_port=8080;
using func_t =function<string(string)>; //这是一个函数
class Udpserver
{
public:
Udpserver(func_t cb, uint16_t port=default_port)
:service_(cb)
,port_(port)
{
cout<<"server addr: "<<port_<<endl;
}
void InitServer()
{
// 1. 创建socket接口, 打开网络文件(本质)
sock_=socket(AF_INET,SOCK_DGRAM,0);
if(sock_<0)
{
cerr<<"create socket error: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout<<"create socket success: "<<sock_<<endl; // sock_ = 3
// 2. 给服务器指明IP地址和Port端口号
// 填充一下服务器的IP和Port
struct sockaddr_in local; // 里面有很多字段 local是在用户空间的特定函数栈帧上,不在内核中!
// 清空local
bzero(&local,sizeof(local)); // 用memset也可以
// 填充sockaddr_in结构
local.sin_family=AF_INET;
local.sin_port=htons(port_); // 端口号要出现在网络中, 主机序列转网络序列
// 使用 inet_addr就可以做下面两件事情:
// (1) 字符串风格的IP地址,转换成为4字节int --- 不能强转, 强转只能改变类型, 不改变二进制构成
// (2) 需要将主机序列转化成为网络序列
// (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
local.sin_addr.s_addr=INADDR_ANY; // 让我们的udp_server在启动的时候, bind本主机上的任意IP
// 把套接字字段和文件字段进行关联 --- 网络文件
int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
cerr<<"bind socket error: "<<strerror(errno)<<endl;
exit(BIND_ERR);
}
cout<<"bind socket success: "<<sock_<<endl;
}
void Start()
{
char buffer[1024]; // 保存用户数据的缓冲区
while(true)
{
// 收到来自客户端发送的消息
struct sockaddr_in peer; // 远端
socklen_t len = sizeof(peer); // 这里一定要写清楚, 未来你传入的缓冲区大小
// 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer, &len);
if(n>0) // 读取数据成功
buffer[n]='\0';
else
continue;
// 提取client信息 --- debug
string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
uint16_t clientport=ntohs(peer.sin_port); // 网络序列转主机序列
cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;
// 做业务处理
string response=service_(buffer);
// 把消息发给别人
// 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
// 谁给我发的, 我就把消息转给谁
// peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
sendto(sock_,response.c_str(),response.size(),0,(struct sockaddr*)&peer, sizeof(peer));
}
}
~Udpserver()
{}
private:
int sock_; //套接字(文件描述符)
uint16_t port_; //端口号(本地主机序列构建的port)
func_t service_; //我们的网络服务器刚刚解决的是网络IO的问题, 要进行业务处理(一个类内的回调方法)
};
}
udp_server.cc
#include"udp_server.hpp"
using namespace ns_server;
// 运行格式: ./udp_server port
// 使用手册
static void usage(string proc)
{
cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}
// 上层的业务处理, 不关心网络发送, 只负责信息处理即可
static bool isPass(string &command)
{
auto pos=command.find("rm");
if(pos!=string::npos) return false;
pos=command.find("mv");
if(pos!=string::npos) return false;
pos=command.find("while");
if(pos!=string::npos) return false;
pos=command.find("kill");
if(pos!=string::npos) return false;
return true;
}
// 让同学们, 在你的本地把命令给我, server再把结果给你!
string excuteCommand(string command) // command就是一个命令
{
// 1. 安全检查
if(!isPass(command))
return "you are a bad man";
// 2. 业务逻辑处理
FILE*fp=popen(command.c_str(),"r");
if(fp==nullptr)
return "None";
// 3. 获取结果
char line[1024];
string ret;
while(fgets(line,sizeof(line),fp)!=NULL)
{
ret+=line;
}
pclose(fp);
return ret;
}
int main(int argc,char*argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=atoi(argv[1]); //命令行参数转换成uint16_t类型
unique_ptr<Udpserver> usvr(new Udpserver(excuteCommand,port));
usvr->InitServer(); // 服务器初始化
usvr->Start();
return 0;
}
udp_server.cc
#pragma once
#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试
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]);
// 1. 创建套接字
int sock=socket(AF_INET,SOCK_DGRAM,0);
if (sock < 0)
{
cerr << "create socket error: " << strerror(errno) << endl;
exit(SOCKET_ERR);
}
// 2. 关于客户端的绑定
// client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]
// 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
// client的port要随机让OS分配防止client出现启动冲突
// server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化
// 明确服务器是谁
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());
// 3. 向服务器发送消息(这里暂时由用户充当)
while(true)
{
// 用户输入
string message;
cout<< "[遇健的服务器]# ";
getline(cin,message);
// 什么时候bind呢?
// 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建 发送的数据报文
// 发送
sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server, sizeof(server));
// 把消息再收回来(回显回来)
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<<"server echo# "<<buffer<<endl;
}
}
}
运行结果:
(4) v4_多线程版本的群聊系统
v4在v3的基础上加入了之前写的生产消费者模型,多线程实现了群聊系统
LockGuard.hpp
#include<iostream>
#include<pthread.h>
using namespace std;
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; //锁的指针
};
RingQueue.hpp
#include<iostream>
#include<pthread.h>
#include<vector>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
#include<semaphore.h>
#include<mutex>
using namespace std;
// 生产者和消费者要有自己的下标来表征生产和消费要访问哪个资源
static const int N=5;
template<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) // 对应生产者
{
// 1.信号量的好处:
// 可以不用在临界区内部做判断, 就可以知道临界资源的使用情况
// 2.什么时候用锁, 什么时候用sem? --- 你对应的临界资源, 是否被整体使用!
// 生产 --- 先要申请信号量
// 信号量申请成功 - 则一定能访问临界资源
P(_space_sem);
Lock(_p_mutex);
// 一定要有对应的空间资源给我!不用做判断, 是哪一个资源给生产者呢
_ring[_p_step++]=in;
_p_step%=_cap;
V(_data_sem);
Unlock(_p_mutex);
}
void pop(T*out) // 对应消费者
{
// 消费
P(_data_sem); // 1. 先申请信号量是为了更高效
Lock(_c_mutex); // 2.
*out=_ring[_c_step++];
_c_step%=_cap;
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 _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;
};
udp_server.hpp
#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
#include<unordered_map>
#include"ringQueue1.hpp"
#include"lockGuard.hpp"
#include"thread.hpp"
using namespace std;
// 群聊系统 --- 一个线程收消息, 一个线程发消息
// 标识一个客户端: ip+port , 使用unordered_map构建<ip+port, 客户端套接字>来表示某个用户发的消息
namespace ns_server
{
const static uint16_t default_port=8080;
using func_t =function<string(string)>; //这是一个函数
class Udpserver
{
public:
Udpserver(uint16_t port=default_port)
:port_(port)
{
cout<<"server addr: "<<port_<<endl;
pthread_mutex_init(&_lock,nullptr);
p=new Thread(1,bind(&Udpserver::Recv,this));
c=new Thread(2,bind(&Udpserver::Broadcast,this));
}
void StartServer()
{
// 1. 创建socket接口, 打开网络文件(本质)
sock_=socket(AF_INET,SOCK_DGRAM,0);
if(sock_<0)
{
cerr<<"create socket error: "<<strerror(errno)<<endl;
exit(SOCKET_ERR);
}
cout<<"create socket success: "<<sock_<<endl; // sock_ = 3
// 2. 给服务器指明IP地址和Port端口号
// 填充一下服务器的IP和Port
struct sockaddr_in local; // 里面有很多字段 local是在用户空间的特定函数栈帧上,不在内核中!
// 清空local
bzero(&local,sizeof(local)); // 用memset也可以
// 填充sockaddr_in结构
local.sin_family=AF_INET;
local.sin_port=htons(port_); // 端口号要出现在网络中, 主机序列转网络序列
// 使用 inet_addr就可以做下面两件事情:
// (1) 字符串风格的IP地址,转换成为4字节int --- 不能强转, 强转只能改变类型, 不改变二进制构成
// (2) 需要将主机序列转化成为网络序列
// (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
local.sin_addr.s_addr=INADDR_ANY; // 让我们的udp_server在启动的时候, bind本主机上的任意IP
// 把套接字字段和文件字段进行关联 --- 网络文件
int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
cerr<<"bind socket error: "<<strerror(errno)<<endl;
exit(BIND_ERR);
}
cout<<"bind socket success: "<<sock_<<endl;
p->run();
c->run();
}
void addUser(const string&name,const struct sockaddr_in&peer)
{
// online[name]=peer
LockGuard lockguard(&_lock);
auto iter=onlineuser.find(name);
if(iter!=onlineuser.end()) // 存在(找到了)直接返回
return;
onlineuser.insert(make_pair(name,peer)); // 不存在(没找到)就插入
}
void Recv()
{
char buffer[1024]; // 保存用户数据的缓冲区
while(true)
{
// 收到来自客户端发送的消息
struct sockaddr_in peer; // 远端
socklen_t len = sizeof(peer); // 这里一定要写清楚, 未来你传入的缓冲区大小
// 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer, &len);
if(n>0) // 读取数据成功
buffer[n]='\0';
else
continue;
// 提取client信息 --- debug
string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
uint16_t clientport=ntohs(peer.sin_port); // 网络序列转主机序列
cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;
// 构建一个用户, 并检查
string name=clientip;
name+="-";
name+=to_string(clientport);
// 构建哈希表来存储用户 - 如果不存在,就插入;如果存在,什么都不做
addUser(name,peer);
string message=name+">>"+buffer;
_rq.push(message); // 消息放入环形队列中
}
}
// 发消息 --- 给所有在线用户
void Broadcast()
{
while(true)
{
string sendstring;
_rq.pop(&sendstring); // 从环形队列中读到了消息
vector<struct sockaddr_in> v; // 把需要发送的信息放到(拷贝)一个数组中<这是内存级的拷贝>
{
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));
cout<<"send done ..."<<sendstring<<endl;
}
}
}
~Udpserver()
{
pthread_mutex_destroy(&_lock);
p->join();
c->join();
delete p;
delete c;
}
private:
int sock_; //套接字(文件描述符)
uint16_t port_; //端口号(本地主机序列构建的port)
unordered_map<string, struct sockaddr_in> onlineuser; // 保存在线用户 --- 需要加锁保证安全
pthread_mutex_t _lock;
RingQueue<string> _rq;
Thread*p;
Thread*c;
};
}
udp_server.cc
#include"udp_server.hpp"
using namespace ns_server;
// 运行格式: ./udp_server port
// 使用手册
static void usage(string proc)
{
cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}
// 上层的业务处理, 不关心网络发送, 只负责信息处理即可
int main(int argc,char*argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=atoi(argv[1]); //命令行参数转换成uint16_t类型
unique_ptr<Udpserver> usvr(new Udpserver(port));
usvr->StartServer();
return 0;
}
udp_client.cc
#pragma once
#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试
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]);
// 1. 创建套接字
int sock=socket(AF_INET,SOCK_DGRAM,0);
if (sock < 0)
{
cerr << "create socket error: " << strerror(errno) << endl;
exit(SOCKET_ERR);
}
// 2. 关于客户端的绑定
// client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]
// 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
// client的port要随机让OS分配防止client出现启动冲突
// server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化
// 明确服务器是谁
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());
// 3. 向服务器发送消息(这里暂时由用户充当)
while(true)
{
// 用户输入
string message;
cout<< "[遇健的服务器]# ";
getline(cin,message);
// 什么时候bind呢?
// 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建发送的数据报文
// 发送
sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server, sizeof(server));
// 把消息再收回来(回显回来)
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<<"server echo# "<<buffer<<endl;
}
}
}
运行结果:
(5) v5_Windows与Linux配合聊天室
我们可以以Linux云服务器作为服务端,Windows作为客户端,在Windows下我们要修改成Windows下的接口,同时开放云服务器的端口号,使用v4版本的服务端代码
Windows下的客户端
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<WinSock2.h>
#include<string>
#include<cstring>
using namespace std;
#pragma warning(disable:4996)
#pragma comment(lib,"ws2_32.lib")
uint16_t serverport = 8080;
std::string serverip = "47.108.235.67";
//std::string serverip = "127.0.0.1";
int main()
{
WSADATA WSAData;
if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
{
cerr << "init error" << endl;
return -1;
}
SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
cerr << "create socket error: " << strerror(errno) << 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());
// 3. 向服务器发送消息(这里暂时由用户充当)
while (true)
{
// 用户输入
string message;
cout << "Please Enter Your Message# ";
getline(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';
cout << buffer << endl; // 往1号文件描述符输出
}
}
closesocket(sock);
WSACleanup();
return 0;
}
运行结果: