【Linux】手把手教你实现udp服务器
网络套接字~
文章目录
前言一、udp服务器的实现总结
前言
上一篇文章中我们讲到了很多的网络名词以及相关知识,下面我们就直接进入udp服务器的实现。
一、udp服务器的实现
首先我们需要创建五个文件(文件名可以自己命名也可以和我一样),分别是makefile,udpclient.cc,udpclient.hpp,udpserver.cc,udpserver.hpp,下面我们先进行makefile的编写,在makefile中我们要一次创建两个可执行程序:
cc=g++
.PHONY:all
all:udpClient udpServer
udpClient:udpClient.cc
$(cc) -o $@ $^ -std=c++11
udpServer:udpServer.cc
$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udpClient udpServer
我们通过all就可以创建多个可执行程序了,对于cc这个变量我们设置为g++,以后如果想换其他的编译器就可以直接替换了。
在udpserver.hpp这个文件中我们先写出整体框架:
namespace Server
{
class udpServer
{
public:
udpServer()
{
}
void InitServer()
{
}
void start()
{
}
~udpServer()
{
}
private:
//服务器一定要有自己的服务端口号(注意端口号是16位的)
uint16_t _port; //端口号
//实际上一款服务器不建议指明一个IP
string _ip; //ip
};
}
那么我们现在服务器的ip填多少呢?实际上我们只是完成测试,所以ip就填0.0.0.0就好了,这样的话任意的ip都能访问我们的服务器,所以我们定义一个static变量来保存ip:
static const string defaultIp = "0.0.0.0";
有了ip和端口号后,我们就可以用构造函数初始化了:
udpServer(const uint16_t& port,const string ip = defaultIp)
:_port(port)
,_ip(ip)
{
}
我们的服务器未来要启动的话就必须先初始化然后再启动,所以我们写了init和start接口,那么该如何初始化呢?实际上不管是udp还是tcp,我们初始化都是需要套接字的,下面我们看看套接字的接口:
如何理解套接字呢,我们都知道linux一切皆文件,所以未来的网络通信一定是在同一个文件中只要和网卡设备关联起来就实现了网络通信,所以套接字的目的实际上是创建一个文件,可以看到我们的套接字有三个参数,第一个参数的解释是域,实际上就是让我们选择是进行网络通信还是本地通信,这里我们一般选择AF_INET选项,代表使用IPV4协议的网络通信。第二个参数是type,表面套接字要向我们提供服务的类型,怎么理解呢,如下图:
我们现在所写的UDP服务器的特点是不可靠传输无连接,而这正是与SOCK_DGRAM这个选项所匹配的,我们查看这个选项的解释可以看到:DGRAM适用于不可靠传输,连接少
我们下一篇要实现的TCP服务器,就会用到SOCK_STREAM这个选项,因为这个选项的解释是面向流式服务,而我们TCP的特点就是面向字节流。
第三个参数我们一般缺省为0,因为这个参数代表我们未来要采用什么协议,如果我们写为0,那么这个接口会根据我们填的前两个参数来帮我们确定第三个参数是选择TCP协议还是UDP协议。
这个接口的返回值相信大家也看到了,没错!一旦创建套接字成功,那么就会给我们返回一个文件描述符,如果失败则会给我们返回-1并且提供错误码。
了解了socket这个接口,那么我们下一步就是增加一个私有变量来接收socket返回的文件描述符(注意:这个文件描述符会被后面的接口多次用到):
然后我们在构造函数中将这个文件描述符初始化为-1:
udpServer(const uint16_t& port,const string ip = defaultIp)
:_port(port)
,_ip(ip)
,_sockfd(-1)
{
}
然后我们初始化第一步:使用套接字
void InitServer()
{
//UDP第一步:创建了一个套接字
_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if (_sockfd==-1)
{
cerr<<"socket error: "<
uint32_t 2.主机转网络,ip是四字节htonl
bzero这个接口可以将我们的结构体里面的内容初始化为0,然后我们进行填充首先协议家族填写AF_INET这里是固定写法,然后就是填写端口号和ip地址,对于端口号,在结构体中的类型是16字节的short短整型,而htons这个接口可以将主机字节序转化为网络字节序(还记得我们上一篇讲的内容吗?网络中所有字节序必须是大端存储,而主机中有可能大端有可能小端,所以hton这个接口就是将任意的主机字节序转换为网络字节序的接口),htons后面的s代表要转化为16字节的,如果你的port是32字节的,那么你就需要用htonl转换为long类型。
对于ip的填充,首先结构体中的ip的类型是32位的,而我们刚刚在类内定义的是一个字符串,所以我们需要先将字符串转换为32位整形,然后再将这个32位整形由主机字节序转化为网络字节序,所以正常的步骤是:1.string->uint32_t 2.htonl(uint32_t) 但是现在我们有一个很好用的接口,这个接口是inet_addr,下面我们看看这个接口:
我们可以看到inet_addr的参数是一个const char*类型,这是什么呢?实际上这个类型就是我们ip常用的点分十进制类型,这个函数的返回值是in_addr_t,也就是说这个函数可以直接将点分十进制类型转化为我们结构体中所需要的ip类型。
我们将这个结构体填充完毕后,下面就直接绑定端口号和ip:
int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if (n==-1)
{
cerr<<"bind error: "< usvr(new udpServer(port,ip));
usvr->InitServer();
usvr->start();
return 0;
}
对于main函数的参数我们之前已经讲过,argc代表你传了几个参数,argv这个数组对应的下标就是我们的参数。我们的目的是:./udpserver ip port这样使用,所以一共有三个参数,如果用户没有传3个参数,那么我们就直接提示如何使用并且退出程序,这里我们也可以弄一个错误码写到枚举中:
enum
{
USAGE_ERR = 1
,SOCKET_ERROR
,BIND_ERR
};
如果用户输入成功,那么我们先获取用户输入的端口号,因为用户输入的是字符串,所以需要将字符串转化为整形,我们用uint16_t的类型来接收端口号,因为我们的server类中的ip是string的,所以可以直接用string变量获取ip地址。然后我们用一个智能指针来管理服务器,在服务器中使用端口号和ip构造服务器,然后对服务器进行初始化和启动即可。
下面我们讲解一个在绑定前填充结构体中ip地址的问题:实际上我们在正在做项目的时候,是不会直接像下面这样指明一个IP的:
真实的写法应该是下面这样:
local.sin_addr.s_addr = INADDR_ANY; //任意地址绑定才是服务器的真实写法
什么意思呢?实际上就是当我们将服务器的IP设为ANY(本质其实是0),就代表未来发给我的数据只要是绑定了我的端口那么就能与我通信,这样就不会漏掉没有我IP地址的服务器给我发的消息了。还记得我们刚开始写的IP是什么吗?没错就是全0,也就是说我们现在写的这个服务器是不需要我们具体的IP只需要通过端口号就可以启动台服务器,并且未来客户端访问我们的服务器的时候是不需要指明IP的,任意一个IP+特定的端口号都能访问我们这台服务器。既然不需要IP,下面我们就修改一下代码:
static void Usage(string proc)
{
cout<<"Usage:\n\t"< usvr(new udpServer(handerMessage,port));
usvr->InitServer();
usvr->start();
return 0;
}
所以实际上一个服务器的IP不重要,只要我们有端口号就能启动这台服务器,并且客户端用任意的IP和我们服务器特定的端口号就可以和我们的服务器通信。
下面我们编写start接口的代码,一旦启动我们就要接受数据,所以我们先认识一个接口:
这个接口的第一个参数是我们创建套接字返回的文件描述符,意思就是我们从哪个套接字里读数据。第二个参数是一个缓冲区,第三个参数是这个缓冲区的长度,2和3这两个参数代表的是你读到的数据要放在哪个缓冲区里,第四个参数是读取方式,这里我们默认填0代表阻塞式读取,也就是说客户端不给我们服务端发消息时,我们就一直等待客户端发消息,这就叫阻塞式读取。第五个参数和第六个参数非常重要,这两个参数是输出型参数,也就是说未来客户端给我们发消息时,会将数据放到缓冲区中,然后会将客户端的端口号和IP放到struct sockaddr*这个结构体当中,第六个参数就是这个结构体的长度,我们可以理解为:我们只需要创建一个空的结构体,然后客户端发消息后这个接口就会将客户端的端口号和IP放到我们自己创建的结构体中。
对于这个接口的返回值,如果成功则会给我们返回读到数据的字节数,如果失败返回-1.
static const int gnum = 1024;
void start()
{
//服务器的本质实际上就是一个死循环
char buffer[gnum];
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //必填
ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(structsockaddr*)&peer,&len); //成功返回字节数
}
}
我们在使用recvfrom接口的时候,对于缓冲区是不用考虑\0的存在的,所以长度是1024-1.然后我们的结构体类型在参数中需要做强制类型转换,理由与上面同理。下面我们思考读到数据该干什么?我们的目的是实现一个udp服务器用来进行简单的聊天,聊天的时候要显示出客户端的ip和端口号,所以我们这样设计:
void start()
{
//服务器的本质实际上就是一个死循环
char buffer[gnum];
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //必填
ssize_t s = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len); //成功返回字节数
//1.数据是什么? 2.谁发的
if (s>0)
{
buffer[s] = 0;
string clientip = inet_ntoa(peer.sin_addr); //1.网络序列 2.int->点分十进制IP
uint16_t clientport = ntohs(peer.sin_port);
string message = buffer;
cout<char*
ntol就是hton相反的转换接口。
获取到string类型的ip后,我们再接收端口号,同样需要转换,然后我们就打印用户端ip[端口号]+用户端发的消息即可。这样服务端的代码就实现完成了。
下面我们开始完成客户端代码:
首先客户端必须要有的是服务端的IP和服务端的port,所以我们先写一个框架:
namespace Client
{
class udpClient
{
public:
udpClient(const string& serverip,const uint16_t &serverport)
:_serverip(serverip)
,_serverport(serverport)
{
}
void InitClient()
{
}
void run()
{
}
~udpClient()
{
}
private:
string _serverip;
uint16_t _serverport;
};
}
前面我们说过,对于服务器而言,ip地址是不重要的,只需要端口号就可以启动服务器,因为一般服务器的IP都是全0,代表任意IP都可以访问,所以我们的客户端只需要随便填一个IP加上特殊的端口号就可以通信了,那么客户端内部ip和port肯定是必须要有的,明白了这个知识我们就先实现一下client.cc的框架:
#include "udpClient.hpp"
#include
using namespace Client;
static void Usage(string proc)
{
cout<<"Usage:\n\t"< ucli(new udpClient(serverip,serverport));
ucli->InitClient();
ucli->run();
return 0;
}
这里的原理和我们服务器写的一模一样,我们就直接编写客户端代码:
首先我们客户端的初始化一定也是需要创建套接字的,既然要创建套接字就必须要有一个变量接收套接字返回的文件描述符:
udpClient(const string& serverip,const uint16_t &serverport)
:_sockfd(-1)
,_serverip(serverip)
,_serverport(serverport)
{
}
然后我们编写初始化函数:
void InitClient()
{
// 1.创建socket
_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if (_sockfd==-1)
{
cerr<<"socket error: "<>message;
sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
}
}
这里我们的客户端要持续的输入所以设为死循环,quit是我们新增的一个成员变量:
以上就是我们客户端的代码了,实际上客户端的代码非常简单,下面我们运行起来:
运行起来后我们可以看到是没问题的,这里也解释了为什么说客户端端口不需要程序员绑定,我们可以看到每次客户端重新登录在服务端显示的端口号都是不一样的,因为这是操作系统自动指定的端口号,而我们的服务端的端口号是唯一的,我们客户端必须输入服务端正确的端口号才能访问服务端,当然小伙伴们也一样将服务端的可执行程序直接发给你们的小伙伴,然后让他们直接通过任意ip+ 你的服务器端口号来和你进行聊天,下面是多人通过网络聊天的界面:
总结
以上就是我们udp服务器的所有内容了,下一篇文章我们将会把这个服务器改造称为英汉互译,大型聊天室等好玩的工具。