目录
一,介绍
二,日志
三,服务端
1,服务器参数
2,创建套接字
3,绑定
3.1 填充套接字信息
3.2 绑定
4,启动服务器
4.1接收客户端请求
4.2记录用户信息
4.3 消息转发
4.4 main函数代码
4.5服务器整体代码
四,客户端
1,输入记录服务端的ip和端口号
2,创建套接字
3,填充服务端套接字信息
4,开始通信
5,通信代码
6,整体代码
五,视频展示
一,介绍
准备写一个基于udp协议的最简单的服务器,实现的功能目前的话就是一个群聊。也就是说所有连接服务器的人说的话都会转发给连接服务器的每一个人。后续学习完成之后还会加入新的东西,谢谢大家。
二,日志
程序的日志其实还是挺重要的,日志写的好,写的全面一点可以更好的帮助我们识别出程序的错误,再针对性的去修改。我们我们单独实现一个日志类来搞一下,当然能也是可以直接cout打印的,但是日志吗肯定有一线重复的东西,搞一个也方便,不用一直解重复的东西。
这里用到了c语言的可变参数,所以会涉及到可变参数的提取。
#pragma once
#include<cstdio>
#include<cstdlib>
#include<cstdarg>
#include<ctime>
#include<cassert>
//调试 注意 警告 致命
const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};
#define DEBUG 0
#define NOTICE 1
#define WARINTNG 2
#define FATAL 3
void logMessage(int level,const char* format,...)
{
assert(level>=DEBUG);
assert(level<=FATAL);
//获取当前用于的名字。
char* name=getenv("USER");
char loginfo[1024];
va_list ap;//ap->char*
va_start(ap,format);
//int vsnprintf(char *str, size_t size, const char *format, va_list ap); 会自己调用va_arg进行提取,但是最后不会调用va_end提取
vsnprintf(loginfo,sizeof(loginfo)-1,format,ap);
va_end(ap);//ap==nullptr
FILE* out=(level==FATAL)?stderr:stdout;
fprintf(out,"%s | %u | %s %s\n",log_level[level],time(nullptr),name==nullptr?"unname":name,loginfo);
}
三,服务端
其实也就是写一个"服务器",只不过这个服务器时简化版的,没有啥功能。
1,服务器参数
每一个服务器都必须有自己的ip和port(端口号),前者标识一台主机,后者标识一个主机上的进程,没有错,服务器也是一个进程,不过他是一个"死循环"进程,因为服务器24小时都可能有用户进行访问。第四个参数时为了记录连接服务器的信息,为了后面的群聊实现。
2,创建套接字
第一个参数是本地通信和网路通信的域,网络通信AF_INET,第二个参数是套接字类型,主要是面向字节流和用户数据报 ,udp是用户数据报。创建好了之后,返回值就相等于一个文件描述符一样,创建失败返回-1.
3,绑定
3.1 填充套接字信息
填充网络协议,比如局域网通信还是网络通信,ip,端口号
3.2 绑定
把前面填充好的套接字信息绑定到进程中。
4,启动服务器
4.1接收客户端请求
recvform函数接收客户端请求。同时最后面俩个参数是输出形参数,为的是带回客户端的套接字信息,这样我们服务端在处理完用户请求之后就可以把处理结果给返回给用户了。
4.2记录用户信息
因为我们设计的是一个群聊嘛。所以一个用户发送的消息,我们会把他转发给所有的用户,所以记录连接的用户,之后进行消息转发。最后又视频展示效果的。
4.3 消息转发
4.4 main函数代码
就是给一个ip和一个端口号,也可以直接给一个端口号,在bind那里我们可以控制的。
4.5服务器整体代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <ctype.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <strings.h>
#include <unordered_map>
#include "Log.hpp"
using namespace std;
static void usage(const string s)
{
cout << "Usage:\n\t" << s << " port [ip]" <<endl;
}
class UdpServer
{
public:
// 初始化端口号、ip、套接字的信息
UdpServer(uint16_t port, string ip = "")
: port_(port), ip_(ip), sockfd_(-1)
{
}
~UdpServer()
{
}
public:
void init()
{
// 1,创建socket套接字 相当于打开了一个文件
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0)
{
logMessage(FATAL, "socket %s %d", strerror(errno), sockfd_);
exit(1);
}
// 不需要换行了,log里面换过了。
logMessage(DEBUG, "socket success: %d", sockfd_);
// 2,填充绑定网络信息 指明 ip_ port_
struct sockaddr_in local; // 注意这里不是socketaddr_in。
bzero(&local, sizeof(local));
// 填充协议家族
local.sin_family = AF_INET;
// 填充端口号,本地转网路
local.sin_port = htons(port_);
// 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip
// INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法
// inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
// ip 地址
local.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str());
// 3,绑定 bind
if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
{
logMessage(FATAL, "bind %s: %d", strerror(errno), sockfd_);
exit(2);
}
logMessage(DEBUG, "bind success :%d ", sockfd_);
}
void start()
{
// 设计服务器的时候,我们要明白服务器都是死循环,因为用户任何时候都可能发起请求。
char inbuffer[1024]; // 接收消息
char outbuffer[1024]; // 发送消息
while (true)
{
// 我们接收客户端的请求,之后要想要客户端的请求,那么我们要知道客户端的信息,这里我们通过一个输出形参数带回客户端信息。
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// udp是没有连接的
int size = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (size > 0)
{
inbuffer[size] = 0;
}
else if (size == -1)
{
logMessage(DEBUG, "recvform %d : %d", strerror(errno), sockfd_);
// 接收请求失败,那就继续接收呗,总不能任性到接收一个客户失败,把服务器挂掉呗。
continue;
}
//logMessage(DEBUG, "recvfrom success: %d ", sockfd_);
// 读取用户请求成功,该处理用户请求,然后把处理结果返回回去。
// 获取用户信息。
string peerip = inet_ntoa(peer.sin_addr);
uint16_t peerport = ntohs(peer.sin_port);
//记录用户数据,没有的话添加,有的话不管了。
checkonlineuser(peerip, peerport, peer);
//打印一下客户端请求,方便调试演练。
logMessage(NOTICE,"[%s : %d ] %s",peerip.c_str(),peerport,inbuffer);
//把这个客户发送的消息回环给所有的客户,就类似于微信群聊
messageroute(peerip,peerport,inbuffer);
}
}
void checkonlineuser(string& peerip, uint16_t peerport, const struct sockaddr_in& peer)
{
string key=peerip;
key+=":";
key+=to_string(peerport);
auto iter=users_.find(key);
if(iter==users_.end())
{
//没有的,要添加
users_[key]=peer;
}else
{
//看需要的话可以做点事,不需要就算了。
}
}
void messageroute(string ip,uint16_t port,string info)
{
//注明:谁发送什么消息。
string message="[";
message+=ip;
message+=":";
message+="]";
message+=info;
for(auto& e:users_)
{
//把消息发送给每一个服务的客户端.
sendto(sockfd_,message.c_str(),message.size(),0,(const struct sockaddr*)&e.second,sizeof(e.second));
}
}
private:
uint16_t port_; // 端口号
string ip_; // ip
int sockfd_; // 套接字信息
unordered_map<string, struct sockaddr_in> users_; // 记录链接的用户套接字. key:谁 value:套接字的信息
};
// struct client{
// struct sockaddr_in peer;
// uint64_t when; //peer如果在when之前没有再给我发消息,我就删除这用户
// }
int main(int argc,char* argv[])
{
//程序名,端口号,ip
//端口号必须有,ip不传的话那我们就设计就随机绑定一个,程序员控制的。
if(argc!=2 && argc!=3)
{
usage(argv[0]);
}
uint16_t port=atoi(argv[1]);//字符串转整形。想清楚,别搞复杂了。
string ip;
if(argc==3)
{
ip=argv[2];
}
UdpServer server(port,ip);
//初始化服务器
server.init();
//启动服务器
server.start();
//服务端暂时就完了。如果不发布不搞守护进程的话.
return 0;
}
四,客户端
1,输入记录服务端的ip和端口号
我们要和服务端通信嘛,这个必须要知道呢。
2,创建套接字
3,填充服务端套接字信息
4,开始通信
sendto向服务端发送请求
当前套接字给服务端套接字。dest_addr发送请求
recvform接收服务端返回的请求结果
5,通信代码
俩个执行流,一个发起请求,一个接收请求的结果。
6,整体代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <ctype.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <strings.h>
#include<pthread.h>
#include"Log.hpp"
using namespace std;
void usage(char* s)
{
cout<<"示范:"<<s<<"server_ip server_port"<<endl;
}
struct sockaddr_in server_;
//读取服务端的响应,并打印出来、
void*recverAndPrint(void* args)
{
while(true)
{
int sockfd=*(int*)args;
char buffer[1024];
struct sockaddr_in tmp;
socklen_t len=sizeof(tmp);
int s=recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&tmp,&len);
if(s>0)
{
buffer[s]=0;
cout<<"server echo # "<<buffer<<endl;
}
}
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(1);
}
//1,命令行设置要访问的服务器。
string server_ip=argv[1];
uint16_t server_port=atoi(argv[2]);
//2.创建客户端
//2.1创建socket
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
logMessage(FATAL,"socket fail ");
exit(2);
}
logMessage(DEBUG,"socket success %d ",sockfd);
//client也是需要绑定自己的信息的,但是不要自己绑定,而是os绑定的。
//因为客户端比较多,你自己绑定不知道哪一个绑定了,哪一个没有绑定,
//如果你绑定的被别人绑定了,那么你就会绑定失败了,所以交给OS就好了。OS给你随机绑定 ip port。安心使用吧。
//server进行绑定时因为server的ip port本来就是被大家熟知的,比如百度的网址一般时不会变化的。
//2.2 填写服务端的信息用来给服务端发送请求。
bzero(&server_,sizeof(server_));
server_.sin_family=AF_INET;
server_.sin_addr.s_addr=inet_addr(server_ip.c_str());
server_.sin_port=htons(server_port);
//可以准备通信了起始这里就,知道服务端信息了,自己信息os再第一次发送消息的时候随机绑定。
//俩个线程流去搞,一个线程流接收服务端的结果,一个线程流给服务端发送请求。
pthread_t t;
pthread_create(&t,nullptr,recverAndPrint,(void*)&sockfd);
//3,开始同行
string buffer;
while(true)
{
cout<<"Please Enter# ";
getline(cin,buffer);
sendto(sockfd,buffer.c_str(),buffer.size(),0,(const struct sockaddr*)&server_,sizeof(server_));
}
return 0;
}
五,视频展示
udp套接字群聊展示
lyh_linux-test: linux代码练习。 - Gitee.com