目录
一、理解IP地址与端口
二、socket套接字
三、TCP与UDP的关系
四、网络字节序
五、socket编程
1.socket()创建套接字
2.填充sockaddr_in 结构体
3.bind() 绑定信息
4.recvfrom()接收消息
5.sendto()发送消息
六、UdpServer代码
一、理解IP地址与端口
IP地址是Internet Protocol(互联网协议)的缩写,是网络上用于标识和定位设备的唯一地址。
他分为公网IP与内网IP,我们通常使用并且查询到的都是内网IP,如下,查询到的就是内网ip。
公网IP地址和内网IP地址共同构成了一个设备的所有IP地址,公网IP地址用于设备与互联网上的其他设备通信,而内网IP地址用于设备在局域网内部通信。
而在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址,直到的从哪里来到哪里去,我们才能将消息发到对方设备上。
但是仅仅只知道IP地址还不够,比如你聊QQ的时候,消息不仅仅要到你的电脑中,还得在QQ中显示出来,因此消息还得认识端口号。
端口号(port)是传输层协议的内容
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用.
由此可以看出,网络通信的本质都是进程间通信,只不过这两个进程不一定在同一台电脑上。ip地址用来表示互联网中唯一的一台主机,端口号用来指定该机器中唯一的进程。
二、socket套接字
这种IP地址和端口号的组合通常被称为套接字(socket)。
- 一般情况,一个端口号和一个进程相关联。
- 特殊情况,一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。
比如A进程绑定了很多端口号,那么往这些端口号发送的数据都会来到A这里。但是B端口号绑定了很多进程,那往B端口号发送数据,到底给哪一个进程呢?如果全部都给岂不是数据会很冗余,因此一端口号不能被多个进程绑定。
三、TCP与UDP的关系
TCP(传输控制协议)和UDP(用户数据报协议)是两种常用的网络传输协议。
TCP的特点
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP的特点
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
目前,我们只需要知道TCP有连接,并且可靠,UDP无连接,并且不可靠,字节流与数据包后续再讨论。
由于TCP有连接并可靠,因此需要付出更多的代价去完成TPC通信,适合如支付、发送机密文件重要场景。
UDP无连接并不可靠,因此要简单一些,适合视频通话、打游戏(低延迟)等场景
四、网络字节序
按数据在计算机存储中的排列方式的分为大端机与小端机。
比如在内存中的数据为 0x11223344
如果是大端机就是低位字节处存高地址,高位字节处存低地址。
而小端机就是低位字节处存低地址,高位字节处存高地址。
既然无法保证所有的电脑都是小端机或者大端机,那么在网络通信的时候,需要考虑数据的字节序问题,通常需要进行字节序的转换。
所以网络规定:所有到达网络的数据,必须是大端的,因此所有从网络收到数据的机器,都知道数据是大端的。
所有数据具在发送到网络时,都必须要做小端到大端的转化,你是大端,直接发给网络就可以,你是小段,先转化完再发给网络。数据在到达机器时,也要做相应的判断,进行合适的转化。
如下接口可以进行相应的转化。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
五、socket编程
如下是socket编程的重要接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
其中有一个struct sockaddr* address。
在网络编程的时候,socket分了很多类别如域间socket、网络socket,原始socket等等每一个socket都有自己独特的一些东西,但他们的本质都是socket。因此我们可以使用struct sockaddr* 这个指针来指向所有的socket。实现了C风格的多态。
今天我们着重学习一下网络socket。
1.socket()创建套接字
使用函数socket(int domain,int type,int protocol)接口创建socket。
网络socket的domain为AF_INIT,使用UDP数据包 type 为 SOCK_DGRAM,protocol默认填0。
Linux一切皆文件,这样我们就可以得到一个文件描述符,后面就可以通过该文件描述符在网络上进行通信。
2.填充sockaddr_in 结构体
有了文件描述符,还需要填充sockaddr_in结构体里面的字段,sockaddr 是一个通用的套接字地址结构体,而sockaddr_in则专门为网络socket设计的,他是用于IPv4套接字的地址结构体。
里面有
- sin_family,代表使用哪个地址族,一般填 AF_INET 代表IPv4
- sin_addr.s_addr,代表那个ip地址
- sin_port,代表那个端口号
3.bind() 绑定信息
现在有了网络套接字并填充了sockaddr_in字段,但是由于现在这些内容还是栈上面变量,因此需要通过bind将该套接字绑定到指定的网络地址上,后面就可以通信了。
4.recvfrom()接收消息
recvfrom可以接受消息,接受消息放到buf里,预期接受len个字节的消息,flags默认为0,代表的阻塞模式,src_addr与addrlen为输入输出参数,输出发送放相关sokect消息。(因为有其他网络中的线程进行发送,你来接受,你输出的是你发送方的套接字地址结构体)
5.sendto()发送消息
接口与recevfrom类似,位于区别是adrlen不需要传地址。
通过这些操作与接口,就可以进行网络通信了,话不多说,我们直接用来看看。
六、UdpServer代码
Comm.hpp(错误码头文件)
#pragma once
//错误码
enum{
Usage_Err = 1,
Socket_Err,
Bind_Err,
};
InetAddr.hpp(网络套接字地址结构体封装)
#pragma once
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
class InetAddr
{
public:
InetAddr(struct sockaddr_in& peer):_addr(peer)
{
_port = ntohs(peer.sin_port); // ntohs网络转主机short
_ip = inet_ntoa(peer.sin_addr); // inet_ntoa sin_addr转点分十进制字符串ip
}
string GetIp()
{
return _ip;
}
uint16_t GetPort()
{
return _port;
}
string PrintDebug()
{
string info = _ip;
info+=":";
info+=to_string(_port);
return info;
}
~InetAddr()
{
}
private:
string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
Log.hpp(日志文件)
#pragma once
#include<iostream>
#include<cstdarg>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
using namespace std;
enum{
Debug = 0,
Info,
Warnig,
Error,
Fatal
};
enum{
Screen = 10,
OneFile,
ClassFile
};
string LevelToString(int level)
{
switch (level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warnig:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "Unkonw";
}
}
const int default_style = Screen;
const string default_filename = "Log.";
const string logdir = "log";
class Log
{
public:
Log(int style = default_style,string filename = default_filename)
:_style(style),_filename(filename)
{
if(_style != Screen)
mkdir(logdir.c_str(),0775);
}
//更改打印方式
void Enable(int style)
{
_style = style;
}
//时间戳转化为年月日时分秒
string GetTime()
{
time_t currtime = time(nullptr);
struct tm* curr = localtime(&currtime);
char time_buffer[128];
snprintf(time_buffer,sizeof(time_buffer),"%d-%d-%d %d:%d:%d",
curr->tm_year+1900,curr->tm_mon+1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec);
return time_buffer;
}
//写入到文件中
void WriteLogToOneFile(const string& logname,const string& message)
{
FILE* fp = fopen(logname.c_str(),"a");
if(fp==nullptr)
{
perror("fopen filed");
exit(-1);
}
fprintf(fp, "%s\n", message.c_str());
fclose(fp);
}
//打印日志
void WriteLogToClassFile(const string& levelstr,const string& message)
{
string logname = logdir;
logname+="/";
logname+=_filename;
logname+=levelstr;
WriteLogToOneFile(logname,message);
}
void WriteLog(const string& levelstr,const string& message)
{
switch (_style)
{
case Screen:
cout<<message<<endl;//打印到屏幕中
break;
case OneFile:
WriteLogToClassFile("all",message);//给定all,直接写到all里
break;
case ClassFile:
WriteLogToClassFile(levelstr,message);//写入levelstr里
break;
default:
break;
}
}
//打印日志
void LogMessage(int level,const char* format,...)
{
char rightbuffer[1024];//处理消息
va_list args; //va_list 是指针
va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
//现在args指向了可变参数部分
vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
va_end(args);
char leftbuffer[1024];//处理日志等级、pid、时间
string levelstr = LevelToString(level);
string currtime = GetTime();
string idstr = to_string(getpid());
snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
,currtime.c_str(),idstr.c_str());
string loginfo = leftbuffer;
loginfo+=rightbuffer;
WriteLog(levelstr,loginfo);
}
//提供接口给运算符重载使用
void _LogMessage(int level,char* rightbuffer)
{
char leftbuffer[1024];
string levelstr = LevelToString(level);
string currtime = GetTime();
string idstr = to_string(getpid());
snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
,currtime.c_str(),idstr.c_str());
string messages = leftbuffer;
messages+=rightbuffer;
WriteLog(levelstr,messages);
}
//运算符重载
void operator()(int level,const char* format,...)
{
char rightbuffer[1024];
va_list args; //va_list 是指针
va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
va_end(args);
_LogMessage(level,rightbuffer);
}
~Log()
{}
private:
int _style;
string _filename;
};
Log lg;
class Conf
{
public:
Conf()
{
lg.Enable(Screen);
}
~Conf()
{}
};
Conf conf;
nocopy.hpp(服务端继承该文件,进栈拷贝构造与赋值拷贝构造)
#pragma once
class nocopy
{
public:
nocopy(){}
nocopy(const nocopy& n) = delete;
nocopy& operator=(const nocopy& n) = delete;
~nocopy(){}
};
UdpServer.hpp(服务器封装的类)
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h> //网络常用4小只
#include <sys/socket.h> //网络常用4小只
#include <netinet/in.h> //网络常用4小只
#include <arpa/inet.h> //网络常用4小只
#include <cerrno>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
using namespace std;
const static string defaultip = "0.0.0.0";
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
class UdpServer :public nocopy
{
public:
//固定绑定ip才需要 _ip
// UdpServer(const string& ip = defaultip,uint16_t port = defaultport)
// :_ip(ip),_port(port),_sockfd(defaultfd)
// {
// }
UdpServer(uint16_t port = defaultport)
:_port(port),_sockfd(defaultfd)
{
}
void Init()
{
//1.创建套接字 AF_INET表示网络套接字 SOCK_DGRAM 表示 UDP 协议的数据报套接字
_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
lg.LogMessage(Fatal,"socket error, %d : %s",errno,strerror(errno));//打印日志
exit(Socket_Err);
}
lg.LogMessage(Info,"socket success, sockfd : %d", _sockfd);
//2.填充 sockaddr_in 结构体 指定网络信息
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family = AF_INET; //表示要使用的是 IPv4 地址
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); //inet_addr将字符串转32位二进制整数(固定绑定)
local.sin_addr.s_addr = INADDR_ANY;//IP动态绑定(不固定)
local.sin_port = htons(_port); //_port主机转网络
//绑定,让设备知道该结构体中的网络与端口
int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n!=0)
{
//绑定失败
lg.LogMessage(Fatal,"bind error, %d : %s",errno,strerror(errno));
exit(Bind_Err);
}
}
void Start()
{
//服务器一直要接受消息,永不退出
char buff[1024];
for(;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//接受消息放到buff里,预期接受sizeof(buff)-1个字节的消息,peer为输入输出参数,输出clien的sokect消息
ssize_t n = recvfrom(_sockfd,buff,sizeof(buff)-1,0,(struct sockaddr*)&peer,&len);
if(n>0)
{
InetAddr addr(peer);
buff[n] = 0;
cout<<"["<<addr.PrintDebug()<<"]# " <<buff<<endl;
//sendto 发送消息给peer
ssize_t m = sendto(_sockfd,buff,strlen(buff),0,(struct sockaddr*)&peer,len);
}
}
}
~UdpServer()
{
}
private:
// string _ip;(固定绑定服务器ip才需要,我们选择不固定,动态绑定
uint16_t _port;
int _sockfd;
};
UdpClient.cc (客户端)
#include<iostream>
#include <cerrno>
#include <cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include "Comm.hpp"
using namespace std;
void Usage(string proc)
{
cout<<"Usage: \n\t" <<proc<<"local_ip local_port\n"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
return Usage_Err;
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
//创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
cerr<<"socket error: "<<strerror(errno)<<endl;
exit(Socket_Err);
}
cout<<"create socket success, sockfd:"<<sockfd<<endl;
//客户端需要bind,但是不需要显示bind(手动发生数据自动bind)
//因为服务器的端口号只有一个,是总所周知并且固定的,客户端会很多并不一定在线
//填充server信息
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_port = htons(serverport);
socklen_t len = sizeof(server);
while(true)
{
string inbuffer;
cout<<"Please Enter#";
getline(cin,inbuffer);
//给server发送消息
ssize_t n = sendto(sockfd,inbuffer.c_str(),inbuffer.size(),0,(struct sockaddr*)&server,len);
if(n > 0)
{
char buff[1024];
//发送成功,并且接收消息
struct sockaddr_in tmp;
socklen_t tmp_len = sizeof(tmp);
ssize_t m = recvfrom(sockfd,buff,sizeof(buff)-1,0,(struct sockaddr*)&tmp,&len);
if(m>0)
{
//接受成功
buff[m] = 0;
cout<<"server echo# "<<buff<<endl;
}
}
}
return 0;
}
Main.cc (服务器的主函数)
#include "UdpServer.hpp"
#include"Comm.hpp"
#include<memory>
//固定绑定ip
// void Usage(string proc)
// {
// cout<<"Usage: \n\t" <<proc<<"local_ip local_port\n"<<endl;
// }
// int main(int argc,char* argv[])
// {
// if(argc!=3)
// {
// Usage(argv[0]);
// return Usage_Err;
// }
// string ip = argv[1];
// uint16_t port = stoi(argv[2]);
// unique_ptr<UdpServer> usvr (new UdpServer(ip,port));
// usvr->Init();
// usvr->Start();
// return 0;
// }
//动态绑定ip
void Usage(string proc)
{
cout<<"Usage: \n\t" <<proc<<"local_port\n"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = stoi(argv[1]);
unique_ptr<UdpServer> usvr (new UdpServer(port));
usvr->Init();
usvr->Start();
return 0;
}
Makefile(进行一键构造)
.PHONY:all
all:udp_server udp_client
udp_server:Main.cc
g++ -o $@ $^ -std=c++11
udp_client:UdpClient.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udp_server udp_client
运行结果如下,客户端可以给服务器发消息,服务器将客户端的发来消息做回应。
代码地址
1.test为本文内容。
2.udp_server_excute为远程发送bash指令
3.udp_server_chat为多人聊天室。
谢谢大家观看!!!