UDP客户端服务器通信
在这篇博客中,我们将探索 UDP(用户数据报协议) 通信,简要地说,UDP 是一种无连接、快速但不可靠的通信协议,适用于需要快速数据传输但对丢包容忍的场景,比如视频流和在线游戏。就像《我是如此相信》的图片所表达的那样,UDP 没有过多的保障机制,它直接、简单、快捷,就像信念一样,直接而坚定。
1、网络协议中的下三层主要解决的是,数据安全可靠得发送到远端机器
2、用户使用应用层软件,完成数据的发送和接收,先把软件启动起来,那就是进程,因此网络通信的本质就是进程间的通信。
一、认识端口号
端口号无论是客户端还是服务器,都能唯一的标识该主机上的一个特定应用层的进程,ip地址唯一的标识一个主机,端口号只能用来标识该主机上的唯一进程。IP+Port能够标识全网唯一的进程。
二、套接字
客户端和服务器通信的时候就好像插头和插座的关系
端口号VS进程pid,但是pid已经可以唯一标识一个进程,为什么还要端口号?
1、不是所有的进程都要网络通信
2、pid属于操作系统中的进程管理,但是要是系统出问题,网络部分也要改,牵一发而动全身,因此要解耦。
我们的客户端怎么知道服务器的端口号?每个服务器的端口必须是众所周知的,被客户端知道。
一个进程可以绑定多个端口号,一个端口号不可以被多个进程绑定。经过哈希找的进程就不一样了
2、 套接字的种类:域间(本地通信)、原始(编写网络工具,抓包)和网络套接字
三、UDP和TCP
TCP(Transmission Control Protocol,传输控制协议)网络必须连通
- 面向连接:在数据传输之前,发送端和接收端需要通过三次握手建立连接。这一过程确保了双方都准备好进行数据传输,并且知道对方的存在和状态,为可靠的数据传输奠定基础。例如,当你使用浏览器访问一个网站时,浏览器与网站服务器之间会先建立 TCP 连接,然后才开始传输网页数据.
- 可靠性高:TCP 协议通过多种机制来确保数据的准确无误传输。它使用确认机制,接收方在收到数据后会向发送方发送确认消息,发送方只有在收到确认后才会继续发送下一部分数据,否则会重传未确认的数据。同时,TCP 还具备重传机制,对于在传输过程中丢失或损坏的数据,发送方会自动重新发送,直到数据被正确接收。此外,流量控制和拥塞控制也是 TCP 保证可靠性的重要手段,它可以根据网络状况和接收方的处理能力,动态调整数据传输的速率,避免网络拥塞导致数据丢失.
- 数据有序:TCP 会对数据进行编号和排序,确保数据按照发送的顺序到达接收端。即使数据在网络中经过不同的路径传输,到达接收端时也会被正确地重组,保证应用程序能够接收到完整、有序的数据。例如,在文件传输过程中,文件的各个部分会按照顺序依次传输并在接收端正确拼接。
- 适用于多种应用:由于其可靠性高,TCP 适用于对数据准确性要求严格的应用场景,如文件传输(FTP)、电子邮件(SMTP、POP3 等)、远程登录(Telnet)、网页浏览(HTTP、HTTPS)等,这些应用需要确保数据的完整性和准确性,不容许数据丢失或出错。
UDP(User Datagram Protocol,用户数据报协议)
- 无连接:UDP 在发送数据之前不需要与接收方建立连接,发送方可以随时向目标地址发送数据报。这使得 UDP 的传输过程更加简单、快速,减少了连接建立和拆除的开销1.
- 不可靠传输:UDP 不提供数据传输的可靠性保证,它不会对数据进行确认、重传或排序等操作。数据报在网络中可能会出现丢失、重复或乱序的情况,并且 UDP 本身不会对这些问题进行处理,而是由应用层来负责处理数据的可靠性问题。不过,在一些对实时性要求较高但对数据准确性要求相对较低的应用中,这种不可靠性是可以接受的,例如在线游戏中的玩家位置信息更新、视频会议中的语音和视频数据传输等,偶尔丢失一两个数据包不会对整体效果产生太大影响1.
- 低延迟:由于不需要建立连接和进行复杂的可靠性控制,UDP 的传输延迟相对较低,能够快速地将数据发送到目标地址。这对于实时性要求高的应用非常有利,如音频流、视频流等实时多媒体应用,可以在保证一定流畅度的前提下,尽可能减少延迟1.
- 高效性:UDP 协议的头部开销较小,只有 8 个字节,相比 TCP 的 20 个字节头部,UDP 可以在相同的网络带宽下传输更多的数据,提高了数据传输的效率1.
- 适用于特定应用:UDP 常用于一些对实时性和效率要求较高,但对数据可靠性要求相对较低的应用场景,如网络视频会议、在线游戏、音频流播放、实时监控等。例如,在网络视频会议中,即使偶尔丢失一些视频帧,也不会对会议的进行产生严重影响,而较低的延迟可以保证参会者之间的交互更加流畅。
四、网络字节序
1、判断是小端还是大端 小权重放到小地址就是小端(小小小)
大端:数据的高位字节存于低地址,低位字节存于高地址。
程序:判断机器是大端还是小端?
#include <stdio.h>
int main() {
int num = 0x12345678;
char *ptr = (char *)#
if (*ptr == 0x78) {
printf("Little - Endian\n");
} else if (*ptr == 0x12) {
printf("Big - Endian\n");
} else {
printf("Error\n");
}
return 0;
}
2、网络规定都是大端 ,低地址高字节
五、UDP客户端服务器通信
步骤:1、创建UDP套接字 下面为创建套接字的接口 socket
第一个参数表示的是使用的协议家族,域到底是什么,第二个表示定义的套接字类型,流,还是数据报,第三个参数表示协议参数,填0就行
返回值 On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
2、绑定 bind
插入:如何快速地将整数ip转为字符串? 比如12345667转为0.0.36.123 ,我们经常用的就是点分十进制,但是我们要知道如何转换。有接口inet_addr
【0,255】.【0,255】.【0,255】.【0,255】
struct ip
{
uint8_t part1; //占一个字节
uint8_t part2;
uint8_t part3;
uint8_t part4;
}
int src_ip=12345678;
struct ip *p=(struct ip*)src_ip; //由于 src_ip 是一个 32 位的整数,而 struct ip 是一个包含四个 uint8_t 类型字段的结构体,直接把 src_ip 赋值给 struct ip 类型的变量是不可能的(类型不兼容)。但是,通过指针,我们可以将 src_ip 的内存解释为 struct ip 类型,并按字节访问它。
to_string(p->part1)+"."+to_string(p->part2)+"."+to_string(p->part3)+"."+to_string(p->part4);
就把四字节转换为字符串风格的ip
结果是0.0.3.182
字符串转整数 ”192.168.50.100“ ”192“ ”168“”50“”100“
//192.168.50.100 转换为 123455556
uint32_t Ip; 4个字节
struct ip* x=(struct ip*)&Ip;
x->part1=stoi("192");
x->part2=stoi("168");
x->part3=stoi("50");
x->part4=stoi("100");
查看服务器启动的命令:netstat -nau
一个关于ip
云服务器禁止直接bind公网ip,一个机器可能有多个ip,bind(IP:0),IP为0叫做任意地址绑定凡是发给我这台主机的数据,我们都要根据端口号向上交付。
一个关于port
【0,1023】属于系统内定的端口号,一般都要有固定的应用层协议使用 http:80 https:443 mysql:3306 我们绑定1024以上的
结论:IP地址绑定0,端口号绑定1024以上的
六、程序实现
Log.hpp
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
// void logmessage(int level, const char *format, ...)
// {
// time_t t = time(nullptr);
// struct tm *ctime = localtime(&t);
// char leftbuffer[SIZE];
// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
// ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
// ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// // va_list s;
// // va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
// // va_end(s);
// // 格式:默认部分+自定义部分
// char logtxt[SIZE * 2];
// snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// // printf("%s", logtxt); // 暂时打印
// printLog(level, logtxt);
// }
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
// int sum(int n, ...)
// {
// va_list s; // char*
// va_start(s, n);
// int sum = 0;
// while(n)
// {
// sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
// n--;
// }
// va_end(s); //s = NULL
// return sum;
// }
Main.cc
#include "UdpServer.hpp"
#include <memory>
#include "Log.hpp"
Log log;
void Usage(std::string proc)
{
std::cout<<"\n\rUsage:"<<proc<<"proc[1024+]"<<std::endl;
}
//./udpserver port
//"192.168.1.123"点分十进制字符串风格的ip地址,一共占了13个字节
int main(int argc,char *argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port=std::stoi(argv[1]); //字符串转整数
std::unique_ptr<UdpServer> svr(new UdpServer());
svr->Init();
svr->Run();
return 0;
}
Makefile
.PHONY:all
all:Udpserver Udpclient
Udpserver:Main.cc
g++ -o $@ $^ -std=c++11
Udpclient:UdpClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:
clean:
rm -f Udpserver Udpclient
Udpclient
#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
void Usage(std::string proc)
{
std::cout<<"\n\rUsage:"<<proc<<"serverip serverport"<<std::endl;
}
// ./udpclient serverip serverport 启动客户端的时候,必须要是ip和端口号是是什么
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip=argv[1];
uint16_t serverport=std::stoi(argv[2]);
//构建服务器信息
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
socklen_t len = sizeof(server);
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
//client要绑定,要有自己的IP和端口,只不过不需要用户显示的bind,一般由os随机选择!肯定是客户端先向服务器发起请求,端口号能唯一标识客户端就行,随机分配,但是服务器的端口号必须要准确知道,不然客户端绑定哪一个
//系统什么时候给我Bind,首次放松数据的时候就绑定,sendto
string message;
char buffer[1024];//接收时候的buffer
while(1)
{
cout<<"Please Enter@";
std::getline(cin,message);
cout<<message<<endl;
//1.数据 2.给谁发 &server 发送给server
sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
//服务器发消息处理了之后,再给客户端发回来,客户端就要去接收
ssize_t s=recvfrom(sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len); //倒数第二个是谁发的
if(s>0)
{
buffer[s]=0;
}
cout<<buffer<<endl;
}
close(sockfd);
return 0;
};
Udpserver
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
#include "Log.hpp"
extern Log log;
enum{
SOCKET_ERR=1,
BIND_ERR=2
};
uint16_t defaultport=8080;
std::string defaultip="0.0.0.0";
const int size=1024;
class UdpServer
{
public:
UdpServer(const uint16_t &port=defaultport,const std::string &ip=defaultip):port_(port),sockfd_(0),ip_(ip),isrunning_(false)//一个服务器要启动起来,端口号要有,Ip地址也要有
{
}
void Init()
{
sockfd_=socket(AF_INET,SOCK_DGRAM,0);//创建套接字
if(sockfd_<0)
{
log(Fatal,"socket create error,sockfd:%d",sockfd_);
exit(SOCKET_ERR);
}
printf("socket create success,sockfd:%d\n",sockfd_);
log(Info,"socket create success,sockfd:%d",sockfd_);
struct sockaddr_in local;
bzero(&local,sizeof(local));//把结构体里面的内容全部清零 ,第二个参数是缓冲区的大小
local.sin_family=AF_INET;
local.sin_port=htons(port_); //我要给你发数据,你必须知道我的端口号,我要把我的端口号发给你,因此端口号是需要在网络中传输的,必须是网络字节序列
//local.sin_addr.s_addr=inet_addr(ip_.c_str()); //1.string->uint32_t 将string转char* string.c_str()
local.sin_addr.s_addr=INADDR_ANY;//0,任意绑定,因为是全0,主机转网络不需要做
bind(sockfd_,(const struct sockaddr *)&local,sizeof(local));
}
//服务器应该周而复始的运行
void Run()
{
isrunning_=true;
char inbuffer[size];
while (isrunning_)
{
//完成一次收和一次发
struct sockaddr_in client;//客户端信息
socklen_t len =sizeof(client);
//从哪个套接字中读,读到的消息放到哪里,读多大,谁给发的,结构体大小
ssize_t n=recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,(sockaddr*)&client,&len);
if(n<0)
{
log(Warning,"recvform error");
continue;
}
inbuffer[n]=0;
//对字符串进行加工
std::string info=inbuffer;
std::string echo_string="server echo#"+info;
//再把数据发送回给对方 string的长度 string.size
//你本地的套接字;发送的字符串和长度;默认为0;输入性参数,我知道对方是谁,因为当时对方给我发过消息
sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len);
}
}
~UdpServer()
{
if(sockfd_>0) close(sockfd_);
}
private:
int sockfd_;//网络文件描述符
uint16_t port_; //端口号是两个字节,16个比特位,服务器要有自己的端口号
std::string ip_;//任意地址绑定,填为0
bool isrunning_;
};
改进版:
Main.cc
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include "Log.hpp"
Log log;
void Usage(std::string proc)
{
std::cout<<"\n\rUsage:"<<proc<<"proc[1024+]"<<std::endl;
}
std::string Handler(const std::string &str)
{
std::string res="Server get a message:";
res+=str;
return res;
//服务器收到消息,回调式的调用你传进来的方法,把数进行加工,加工处理完后返回,最后把这个结果发出去
}
std::string ExecuteCommand(const std::string &cmd)
{
// SafeCheck(cmd);
FILE* fp=popen(cmd.c_str(),"r");
if(nullptr==fp)
{
perror("popen");
return "error";
}
std::string result;
//从fp中把命令执行结果都拿出来
char buffer[4096];
while(true)
{
char * res=fgets(buffer,4096,fp);//读到一行
if(nullptr==res) break;
result+=buffer;
}
return result;
pclose(fp);
}
//./udpserver port
//"192.168.1.123"点分十进制字符串风格的ip地址,一共占了13个字节
int main(int argc,char *argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port=std::stoi(argv[1]); //字符串转整数
std::unique_ptr<UdpServer> svr(new UdpServer());
svr->Init();
svr->Run(ExecuteCommand);
return 0;
}
Udpserver.hpp
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
#include <functional>
#include "Log.hpp"
using func_t=std::function<std::string(const std::string&)>; //返回值为string,参数为const std::string&的函数传递给Udpserver
//等价于typedef std::function<std::string(const std::string&)> func_t;
extern Log log;
enum{
SOCKET_ERR=1,
BIND_ERR=2
};
uint16_t defaultport=8080;
std::string defaultip="0.0.0.0";
const int size=1024;
class UdpServer
{
public:
UdpServer(const uint16_t &port=defaultport,const std::string &ip=defaultip):port_(port),sockfd_(0),ip_(ip),isrunning_(false)//一个服务器要启动起来,端口号要有,Ip地址也要有
{
}
void Init()
{
sockfd_=socket(AF_INET,SOCK_DGRAM,0);//创建套接字
if(sockfd_<0)
{
log(Fatal,"socket create error,sockfd:%d",sockfd_);
exit(SOCKET_ERR);
}
printf("socket create success,sockfd:%d\n",sockfd_);
log(Info,"socket create success,sockfd:%d",sockfd_);
struct sockaddr_in local;
bzero(&local,sizeof(local));//把结构体里面的内容全部清零 ,第二个参数是缓冲区的大小
local.sin_family=AF_INET;
local.sin_port=htons(port_); //我要给你发数据,你必须知道我的端口号,我要把我的端口号发给你,因此端口号是需要在网络中传输的,必须是网络字节序列
//local.sin_addr.s_addr=inet_addr(ip_.c_str()); //1.string->uint32_t 将string转char* string.c_str()
local.sin_addr.s_addr=INADDR_ANY;//0,任意绑定,因为是全0,主机转网络不需要做
bind(sockfd_,(const struct sockaddr *)&local,sizeof(local));
}
//服务器应该周而复始的运行
//这样的好处是实现代码的分层
void Run(func_t func)//运行的时候需要用户传进来一个怎么处理数据的方法
{
isrunning_=true;
char inbuffer[size];
while (isrunning_)
{
//完成一次收和一次发
struct sockaddr_in client;//客户端信息
socklen_t len =sizeof(client);
//从哪个套接字中读,读到的消息放到哪里,读多大,谁给发的,结构体大小
ssize_t n=recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,(sockaddr*)&client,&len);
if(n<0)
{
log(Warning,"recvform error");
continue;
}
inbuffer[n]=0;
std::string info=inbuffer;
std::string echo_string =func(info);
sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len);
}
}
~UdpServer()
{
if(sockfd_>0) close(sockfd_);
}
private:
int sockfd_;//网络文件描述符
uint16_t port_; //端口号是两个字节,16个比特位,服务器要有自己的端口号
std::string ip_;//任意地址绑定,填为0
bool isrunning_;
};