【Linux网络编程三】Udp套接字编程(简易版服务器)
- 一.创建套接字
- 二.绑定网络信息
- 1.构建通信类型
- 2.填充网络信息
- ①网络字节序的port
- ②string类型的ip地址
- 3.最终绑定
- 三.读收消息
- 1.服务器端接收消息recvfrom
- 2.服务器端发送消息sendto
- 3.客户端端发送消息sendto
- 4.客户端端接收消息recvfrom
- 四.关于绑定ip与port细节
- 五.客户端不需要主动绑定
- 六.客户端/服务器端代码
UDP套接字编程:
网络通信本质是进程之间通信,所以我们需要两个进程来网络通信,假设一个为服务器进程,一个为客户端进程演示。
一.创建套接字
socket()接口可以用来创建套接字,它总共有三个参数。
第一个参数domain,表示通信的类型,是使用网络通信还是本地通信由用户选择,当传入AF_INET/AF_INET6时表示使用网络通信。
第二个参数type,表示套接字的类型,是TCP呢还是UDP呢。如果传递SOCK_DGRAM表示是UDP,如果传递的SOCK_STREAM表示是TCP.
第三个参数protocol,表示协议,默认为0.
创建套接字成功后会返回一个文件描述符sockfd,也就是创建套接字的本质就是打开一个文件!
服务器端进程在创建完套接字后,该做什么呢?该套接字(文件)是属于你服务器进程的,然后呢?假设客户端也打开一个套接字(文件),这两个套接字文件都是属于同一个,也就是满足了进程间通信的前提条件:能看到一个共享资源。
而两个进程看到同一份资源后,那么该如何准确的发送给对方呢?因为可能打开这个套接字文件的进程有很多。所以接下来就是两个进程需要知道各自对方能够唯一标识自己的ip地址和端口号等网络信息。这样才能够准确的将数据从客户端进程发送给服务器进程。
// 1.创建udp套接字,本质就是打开网络套接字文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 表示创建失败
{
lg(Fatal, "socket create error,socket: %d", _sockfd);
exit(SOCKET_ERR); // 创建失败之间退出
}
二.绑定网络信息
套接字创建成功后,就相当于打开了一个文件,该文件是就两个进程通信的共享资源,不过要想准确通信,还需要知道各自进程的ip地址和端口号,这样往文件里传输数据时,对端才能准确接收到,也就是这个文件需要绑定一些各自进程的网络信息才能准确的传递到对端。
进程之间网络通信需要先绑定端口号,这里创建完套接字后,就需要让该进程的端口号与套接字绑定。这样对端的进程才能找到这个进程
第一个参数就是创建的套接字,也就是文件对象
第二个参数就是要绑定的该进程的网络信息,包括端口号,ip地址等
第三个参数是网络信息结构体对象的大小
这个网络信息结构体对象要求传的是统一的接口,但是你实际使用什么类型的网络通信,你就定义什么类型,然后传递时强转即可。
// 在绑定套接信息之前,需要先将对应的结构体对象填充完毕sock_addr
struct sockaddr_in local; // 网络通信类型
bzero(&local, sizeof(local)); // 将内容置为0
local.sin_family = AF_INET; // 网络通信类型
local.sin_port = htons(_port); // 网络通信中,端口号需要不断发送,所以需要符合网络字节序,主机--->网络字节序
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 需要将string类型的ip转换成int类型,并且还需要满足网络字节序的要求
socklen_t len = sizeof(local);
// 以上只是将要绑定的信息填充完毕,套接字(网络文件)而还没有绑定套接信息
1.构建通信类型
比如如果我要网络通信,那么就需要定义一个sockaddr_in结构体对象。
该结构体对象里有三个需要初始化的参数:
1.sin_family:要使用的通信类型
2.sin_port:该进程的端口号
3.sin_addr:该进程的ip地址
sin_addr这个结构体对象里面只有一个参数,s_addr这个也就是真正的ip地址。
2.填充网络信息
①网络字节序的port
在给套接字绑定网络信息之前,需要将网络信息给构建好,就比如端口号,我们需要将当前进程的端口号填充到sockaddr_in这个结构体对象里。
不过端口号在网络通信中,是要不但的来回发送的,不管是客户端,还是服务器端,两个进程通信就必须知道对方的端口号。
所以端口号是需要发送到网络里的,所以在填充时,必须是网络字节序。
也就是主机转网络字节序
②string类型的ip地址
用户一般喜欢用string类型的ip地址类型,这样比较好显示。
但是系统里的ip是uint16_t类型的,所以我们在填充初始化时.
【要求1】首先需要将string类型的参数转换成uint16_t类型。
【要求2】其次ip地址也需要发送到网络里的,所以也必须是网络字节序。
系统里给了我们相关的接口:inet_addr(char*cp)
它就是可以将string类型的数据转换成uint16_t类型,并且将主机字节序转换成网络字节序。
3.最终绑定
if (bind(_sockfd, (const struct sockaddr *)&local, len) < 0) // 绑定失败
{
lg(Fatal, "bind sockfd error,errno:%d,err string:%s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind sockfd success,errno:%d,err string:%s", errno, strerror(errno)); // 绑定成功
三.读收消息
1.服务器端接收消息recvfrom
一般客户端进程对服务器进程发送消息,服务器进程接收客户端发送的消息。那么服务器进程如何接收客户端发送的消息呢?
服务器进程是从套接字接收消息,也就是该进程创建的文件里接收。
不过服务器除了能够接收到消息,还需要知道是谁给它发送的消息,这样它才可以将消息再发送回去。
所以就需要一个sockaddr_in结构体对象,作为输出型参数,将发送端的网络信息存储下来。也就是将客户端的网络信息带出来。
所以recvfrom的功能
1.除了接收到对端发送的消息内容
2.还可以知道对端的网络消息。知道是谁发送过来的。
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 服务器接收到消息,它还需要知道谁给它发送的,为了后续将应答返回过去
// 利用一个输出型参数,将对方的网络信息填充到里面
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);
if (n < 0)
{
lg(Warning, "recvfrom sockfd err,errno: %d, err string %s", errno, strerror(errno));
continue;
}
// 读取成功,将网络文件里的消息读取到buffer里
buffer[n] = 0; // 字符串形式
2.服务器端发送消息sendto
服务器一般只要用来接收其他客户端的消息,然后加工处理,再将数据发送回去,所以服务器将数据再发送回客户端,也就是往套接字里发送消息,而要发送的客户端网络信息,刚好被存储在输出型参数里。因为客户但是主动发送消息的,服务器一定是先收到客户端发送的消息,然后会将客户端的网络消息存储起来,再根据客户端网络信息发送回去。
// 3.将应答发送回去
// 发送给谁呢?服务器知道吗?服务器知道!因为在接收消息时,服务器就用一个输出型参数,将客户端的网络消息保存下来了
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)&client, len);
因为服务器端在接收客户端发送的消息时,还会保存客户端的网络信息,所以如果服务器想发送消息给客户端是很容易的。
3.客户端端发送消息sendto
客户端将消息发送给服务器端,该怎么发送呢?通过套接字(文件)发送给服务器,发送时需要服务器端的网络信息,比如ip地址端口号等,这样客户端才能准确的发送给该服务器端。
//构建服务器端的网络信息
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_addr.s_addr = inet_addr(serverip.c_str()); // 将string类型转换成int类型,并且是网络字节序
server.sin_port = htons(serverport);
socklen_t len = sizeof(server);
// 1.创建套接字---本质就是打开网络文件
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cout << "socket create err" << endl;
return 1;
}
// 创建成功
// 2.需要绑定吗?系统会给我自动绑定,首次发送的时候就会绑定
char outbuffer[1024];
string message;
getline(cin, message);
//1.发送给服务器
sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&server, len);
4.客户端端接收消息recvfrom
客户端要想接收服务器发送回来的消息,只需要读取套接字里的消息即可。利用recvfrom接口读取客户端的套接字。
不过还需要定义一个结构体对象用来保存服务器端的网络信息,虽然客户端已经知道,但是接口要求,所以必须定义。
struct sockaddr_in temp;
socklen_t l=sizeof(temp);
//2.接收服务器的应答
ssize_t s=recvfrom(sockfd,outbuffer,1023,0,(struct sockaddr*)&temp,&l);
四.关于绑定ip与port细节
【细节1】当服务器端绑定ip时,如果ip地址是’0.0.0.0"则表示任意ip地址绑定。
就表示不管客户端发送时目的ip是多少,只要消息发送到服务器的主机上,那么都可以接收,并将端口号往上发送。
也就是只要是发送到我这台主机上的报文,那么就会忽略到该报文的目的ip地址是多少,只看端口号。
这就表示任意ip地址绑定。相当于一种动态绑定。可以接收到所有发送到我这台主机上的报文。并往上传递。
还要注意在云服务器上,公用ip是不能随意绑定的,无法直接绑定,不管是TCP还是UDP。除非是127.0.0.1这个ip地址是专门用来检测服务器和客户端的。其他的不要瞎绑定。但是在虚拟机上可以。
【细节2】端口号不是随意绑定的,有些是已经被固定使用的,【0,1023】是属于系统内定的端口号,一般要有固定的应用层协议使用。
所以我们最好使用1023后面的。
【总结】
如果服务器端的ip地址默认是0的话,那么我们只需要知道服务器端的端口号即可。
五.客户端不需要主动绑定
所以客户端是不需要显示的绑定相关的端口号和ip地址的。操作系统会帮它自动绑定。
本质原因是用户不关心客户端的端口号和ip地址等网络信息,所以不需要显示绑定。
但是服务器端必须主动绑定端口号!为什么呢?
因为用户关心服务器的端口号,必须知道服务器端的端口号。不然无法找到服务器端。
六.客户端/服务器端代码
【客户端】
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
#include <iostream>
#include <strings.h>
#include <sys/types.h>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
// 客户端
void Usage(std::string proc)
{
std::cout << "\n\r./Usage: " << proc << " serverip serverport\n"
<< endl;
}
// 启动客户端时要求是: ./Client 服务器ip 服务器port
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_addr.s_addr = inet_addr(serverip.c_str()); // 将string类型转换成int类型,并且是网络字节序
server.sin_port = htons(serverport);
socklen_t len = sizeof(server);
// 1.创建套接字---本质就是打开网络文件
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cout << "socket create err" << endl;
return 1;
}
// 创建成功
// 2.需要绑定吗?系统会给我自动绑定,首次发送的时候就会绑定
// 3.往服务器的套接字里发送消息--需要知道服务器的ip和端口号,目的ip和目的port,将ip和port填入结构体对象里
char outbuffer[1024];
string message;
while (true)
{
cout<<"Please enter@ ";
getline(cin, message);
//1.发送给服务器
sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&server, len);
struct sockaddr_in temp;
socklen_t l=sizeof(temp);
//2.接收服务器的应答
ssize_t s=recvfrom(sockfd,outbuffer,1023,0,(struct sockaddr*)&temp,&l);
if(s>0)
{
//接收成功
outbuffer[s]=0;
cout<<outbuffer<<endl;
}
}
close(sockfd);
return 0;
}
【服务器端】
@ -0,0 +1,103 @@
#pragma once
#include "Log.hpp"
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8080;
Log lg; // 日志,默认往显示屏打印
typedef std::function<std::string(const std::string&)> func_t;//相当于定义了一个函数指针
//返回值是string类型,函数参数也是string类型,利用函数回调的方法,将服务器端对数据的处理操作进行分离,由上层传递的函数来决定如何处理
enum
{
SOCKET_ERR = 1,
BIND_ERR
};
class Udpserver
{
public:
Udpserver(const uint16_t &port = defaultport, std::string &ip = defaultip) : _sockfd(0), _port(port), _ip(ip)
{
}
void Init()
{
// 1.创建udp套接字,本质就是打开网络套接字文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 表示创建失败
{
lg(Fatal, "socket create error,socket: %d", _sockfd);
exit(SOCKET_ERR); // 创建失败之间退出
}
// 创建成功
lg(Info, "socket create success,socket: %d", _sockfd);
// 2.绑定服务器的套接信息,比如ip和端口号
// 在绑定套接信息之前,需要先将对应的结构体对象填充完毕sock_addr
struct sockaddr_in local; // 网络通信类型
bzero(&local, sizeof(local)); // 将内容置为0
local.sin_family = AF_INET; // 网络通信类型
local.sin_port = htons(_port); // 网络通信中,端口号需要不断发送,所以需要符合网络字节序,主机--->网络字节序
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 需要将string类型的ip转换成int类型,并且还需要满足网络字节序的要求
socklen_t len = sizeof(local);
// 以上只是将要绑定的信息填充完毕,套接字(网络文件)而还没有绑定套接信息
if (bind(_sockfd, (const struct sockaddr *)&local, len) < 0) // 绑定失败
{
lg(Fatal, "bind sockfd error,errno:%d,err string:%s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind sockfd success,errno:%d,err string:%s", errno, strerror(errno)); // 绑定成功
}
void Run(func_t func) // 服务器是一旦启动不会退出,服务器接收消息,并发送答应
{
// 1.接收信息
char buffer[SIZE];
while (true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 服务器接收到消息,它还需要知道谁给它发送的,为了后续将应答返回过去
// 利用一个输出型参数,将对方的网络信息填充到里面
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&client, &len);
if (n < 0)
{
lg(Warning, "recvfrom sockfd err,errno: %d, err string %s", errno, strerror(errno));
continue;
}
// 读取成功,将网络文件里的消息读取到buffer里
buffer[n] = 0; // 字符串形式
// 2.加工处理
// std::string info = buffer;
// std::string echo_string = "server echo# " + info;
std::string info=buffer;
std::string echo_string=func(info);
//将接收的信息由外层函数进行处理
// 3.将应答发送回去
// 发送给谁呢?服务器知道吗?服务器知道!因为在接收消息时,服务器就用一个输出型参数,将客户端的网络消息保存下来了
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const struct sockaddr *)&client, len);
}
}
~Udpserver()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd; // 套接字文件描述符
std::string _ip; // 我们习惯用string类型的ip地址
uint16_t _port; // 服务器进程的端口号
};
#include "Udpserver.hpp"
#include <memory>
#include <cstdio>
#include <stdlib.h>
// "120.78.126.148" 点分十进制字符串风格的IP地址
std::string handler(const std::string &info)
{
std::string res="get a message: ";
res+=info;
std::cout<<res<<std::endl;
return res;//最后将处理的数据返回回去
}
#include "Udpserver.hpp"
#include <memory>
#include <cstdio>
void Usage(std::string proc)
{
std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//服务器进程,启动时,按照./Udpserver+port的形式传递
int main(int args,char* argv[])
{
if(args!=2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port=std::stoi(argv[1]);
std::unique_ptr<Udpserver> svr(new Udpserver(port));//首先创建一个服务器对象指针
//智能指针,用一个UdpServer指针来管理类对象
svr->Init();//初始化服务器
svr->Run(handler);//启动服务器
return 0;
}