文章目录
- 一、引入
- 二、服务端实现
- 2.1 创建套接字socket
- 2.2 绑定bind
- 2.3 启动服务器
- 2.4 IP的绑定
- 2.5 读取数据recvfrom
- 三、用户端实现
- 3.1 绑定问题
- 3.2 发送数据sendto
- 四、源码
一、引入
在上一章【网络编程】socket套接字中我们讲述了TCP/UDP协议,这一篇就是简单实现一个UDP协议的网络服务器。
我们也讲过其实网络通信的本质就是进程间通信。而进程间通信无非就是读和写(IO)。
所以现在我们就要写一个服务端(server)接收数据,客户端(client)发送数据。
二、服务端实现
通过上一章的介绍,要通信首先需要有IP地址,和绑定端口号。
uint16_t _port;
std::string _ip;
2.1 创建套接字socket
在通信之前要先把网卡文件打开。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
RETURN VALUE
On success, a file descriptor for the new socket is returned.
On error, -1 is returned, and errno is set appropriately.
这个函数的作用是打开一个文件,把文件和网卡关联起来。
参数介绍:
domain
:一个域,标识了这个套接字的通信类型(网络或者本地)。
只用关注上面两个类,第一个AF_UNIX
表示本地通信,而AF_INET
表示网络通信。
type
:套接字提供服务的类型。
这一章我们讲的式UDP,所以使用SOCK_DGRAM
。
protocol
:想使用的协议,默认为0即可,因为前面的两个参数决定了,就已经决定了是TCP还是UDP协议了。
返回值:
成功则返回打开的文件描述符(指向网卡文件),失败返回-1。
而从这里我们就联想到系统中的文件操作,未来各种操作都要通过这个文件描述符,所以在服务端类中还需要一个成员变量表示文件描述符。
static const std::string defaultip = "0.0.0.0";// 默认IP
class UDPServer
{
public:
UDPServer(const uint16_t& port, const std::string ip = defaultip)
: _port(port)
, _ip(ip)
, _sockfd(-1)
{}
void InitServer()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
}
private:
uint16_t _port;
std::string _ip;
int _sockfd;
};
创建完套接字后我们还需要绑定IP和端口号。
2.2 绑定bind
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
RETURN VALUE
Upon successful completion, bind() shall return 0;
otherwise, -1 shall be returned and errno set to indicate the error.
参数介绍:
socket
:创建套接字的返回值。
address
:通用结构体(上一章【网络编程】socket套接字有详细介绍)。
address_len
:传入结构体的长度。
所以我们要先定义一个sockaddr_in
结构体填充数据,在传递进去。
struct sockaddr_in {
short int sin_family; // 地址族,一般为AF_INET或PF_INET
unsigned short int sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IP地址
unsigned char sin_zero[8]; // 用于填充,使sizeof(sockaddr_in)等于16
};
创建结构体后要先清空数据(初始化),我们可以用memset,系统也提供了接口:
#include <strings.h>
void bzero(void *s, size_t n);
填充端口号的时候要注意端口号是两个字节的数据,涉及到大小端问题。
大小端转化接口:
#include <arpa/inet.h>
// 主机序列转网络序列
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
// 网络序列转主机序列
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
对于IP,首先我们要先转成整数,再要解决大小端问题。
系统给了直接能解决这两个问题的接口:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);// 点分十进制字符串
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
这里的inet_addr
就是把一个点分十进制的字符串转化成整数再进行大小端处理。
整体代码:
void InitServer()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
// 绑定IP与port
struct sockaddr_in si;
bzero(&si, sizeof si);
si.sin_family = AF_INET;// 协议家族
si.sin_port = htons(_port);// 端口号,注意大小端问题
si.sin_addr.s_addr = inet_addr(_ip.c_str());// ip
// 绑定
int n = bind(_sockfd, (struct sockaddr*)&si, sizeof si);
assert(n != -1);
}
2.3 启动服务器
首先要知道服务器要死循环,永远不退出,除非用户删除。站在操作系统的角度,服务器是常驻内存中的进程。
而我们启动服务器的时候要传递进去IP和端口号。
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "incorrect number of parameters" << std::endl;
exit(1);
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
std::unique_ptr<UDPServer> ptr(new UDPServer(port, ip));
return 0;
}
那么IP该怎么传递呢?
2.4 IP的绑定
这里的127.0.0.1叫做本地环回
它的作用就是用来做服务器代码测试的,意思就是如果我们绑定的IP是127.0.0.1的话,在应用层发送的消息不会进入物理层,也就不会发送出去。
当我们运行起来后想要查看网络情况就可以用指令netstat
后边也可以附带参数:
-a:显示所有连线中的Socket;
-e:显示网络其他相关信息;
-i:显示网络界面信息表单;
-l:显示监控中的服务器的Socket;
-n:直接使用ip地址(数字),而不通过域名服务器;
-p:显示正在使用Socket的程序识别码和程序名称;
-t:显示TCP传输协议的连线状况;
-u:显示UDP传输协议的连线状况;
那我们如果想要全网通信呢?该用什么IP呢?难道是云服务器上的公网IP吗?
但是我们发现绑定不了。
因为云服务器是虚拟化服务器(不是真实的IP),不能直接绑定公网IP。
既然公网IP邦绑定不了,那么内网IP(局域网IP)呢?
答案是可以,说明这个IP是属于这个服务器的
但是这里不是一个内网的就无法找到。
所以现在的问题是服务器启动后怎么收到信息呢?(消息已经发送到主机,现在要向上交付)
实际上,一款服务器不建议指明一个IP。因为可能服务器有很多IP,如果我们绑定了一个比如说IP1,那么其他进程发送给IP2服务器就收不到了。
这里的INADDR_ANY
实际上就是0,这样绑定后,发送到这台主机上所有的数据,只要是访问绑定的端口(8080)的,服务器都能收到。这样就不会因为绑定了一个具体的IP而漏掉其他IP的信息
static const std::string defaultip = "0.0.0.0";// 默认IP
所以现在我们就不需要传递IP了。
2.5 读取数据recvfrom
服务端要获取到用户端发送过来的数据。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
sockfd
:从哪个套接字读。
buf
:数据放入的缓冲区。
len
:缓冲区长度。
flags
:读取方式。 0代表阻塞式读取。
src_addr
和addrlen
:输出型参数,返回对应的消息内容是从哪一个客户端发出的。第一个是自己定义的结构体,第二个是结构体长度。
void start()
{
char buf[1024];
while(1)
{
struct sockaddr_in peer;
socklen_t len = sizeof peer;
sssize_t s = recvfrom(_sockfd, buf, sizeof buf - 1, 0, (struct sockaddr*)&peer, &len);
}
}
现在我们想要知道是谁发送过来的消息,信息都被保存到了peer结构体中,我们知道IP信息在peer.sin_addr.s_addr
中,首先这是一个网络序列,要转成主机序列,其次为了方便观察,要把它转换成点分十进制。
而这两个操作系统给了一个接口能够解决:
char *inet_ntoa(struct in_addr in);
同样获取端口号的时候也要由网络序列转成主机序列:
uint16_t ntohs(uint16_t netshort);
整体代码:
void start()
{
char buf[1024];
while(1)
{
struct sockaddr_in peer;
socklen_t len = sizeof peer;
ssize_t s = recvfrom(_sockfd, buf, sizeof buf - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
std::string cip = inet_ntoa(peer.sin_addr);
uint16_t cport = ntohs(peer.sin_port);
std::string msg = buf;
std::cout << " [" << cip << "@" << cport << " ]# " << msg << std::endl;
}
}
}
现在只需要等待用户端发送数据即可。
三、用户端实现
首先我们要发送数据,就得知道客户端的IP和port。
而这里的IP就必须指明。
uint16_t _serverport;
std::string _serverip;
int _sockfd;
这里的IP和port指的是要发送给谁。
创建套接字就跟前面的一样:
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
3.1 绑定问题
这里的客户端必须绑定IP和端口,来表示主机唯一性和进程唯一性。
但是不需要显示的bind。
那么为什么前面服务端必须显示的绑定port呢?
因为服务器的端口号是众所周知的,不能改变,如果变了就找不到服务器了。
而客户端只需要有就可以,只用标识唯一性即可。
举个例子:
我们手机上有很多的app,而每个服务端是一家公司写的,但是客户端却是多个公司写的。如果我们绑定了特定的端口,万一两个公司都用了同一个端口号呢?这样就直接冲突了。
所以操作系统会自动形成端口进行绑定。(在发送数据的时候自动绑定)
所以创建客户端我们只用创建套接字即可。
void InitClient()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
}
3.2 发送数据sendto
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
这里的参数和上面的recvfrom差不多,而这里的结构体内部我们要自己填充目的IP和目的端口号。
void start()
{
struct sockaddr_in si;
bzero(&si, sizeof(si));
si.sin_family = AF_INET;
si.sin_addr.s_addr = inet_addr(_serverip.c_str());
si.sin_port = htons(_serverport);
std::string msg;
while(1)
{
std::cout << "Please input: ";
std::cin >> msg;
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&si, sizeof si);
}
}
当然这里是同一台主机之间测试,如果是不同的机器,我们传递参数的时候就要传递公网IP,例如我们这台云服务器的公网IP是:
我们在运行的时候:
./UDPClient 43.143.106.44 8080
四、源码
// UDPServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <strings.h>
#include <netinet/in.h>
#include <string.h>
#include <cassert>
#include <functional>
static const std::string defaultip = "0.0.0.0";// 默认IP
class UDPServer
{
public:
UDPServer(const uint16_t& port, const std::string ip = defaultip)
: _port(port)
, _ip(ip)
, _sockfd(-1)
{}
void InitServer()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
// 绑定IP与port
struct sockaddr_in si;
bzero(&si, sizeof si);
si.sin_family = AF_INET;// 协议家族
si.sin_port = htons(_port);// 端口号,注意大小端问题
// si.sin_addr.s_addr = inet_addr(_ip.c_str());// ip
si.sin_addr.s_addr = INADDR_ANY;
// 绑定
int n = bind(_sockfd, (struct sockaddr*)&si, sizeof si);
assert(n != -1);
}
void start()
{
char buf[1024];
while(1)
{
struct sockaddr_in peer;
socklen_t len = sizeof peer;
ssize_t s = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buf[s] = 0;// 结尾
std::string cip = inet_ntoa(peer.sin_addr);
uint16_t cport = ntohs(peer.sin_port);
std::string msg = buf;
std::cout << "[" << cip << "@" << cport << "]# " << msg << std::endl;
}
}
}
private:
uint16_t _port;
std::string _ip;
int _sockfd;
};
// UDPServer.cc
#include "UDPServer.hpp"
#include <memory>
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cout << "incorrect number of parameters" << std::endl;
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<UDPServer> ptr(new UDPServer(port));
ptr->InitServer();
ptr->start();
return 0;
}
// UDPClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <strings.h>
#include <netinet/in.h>
#include <string.h>
#include <cassert>
class UDPClient
{
public:
UDPClient(const std::string& serverip, const uint16_t& port)
: _serverip(serverip)
, _serverport(port)
, _sockfd(-1)
{}
void InitClient()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
std::cerr << "socket error" << errno << " : " << strerror(errno) << std::endl;
exit(1);
}
}
void start()
{
struct sockaddr_in si;
bzero(&si, sizeof(si));
si.sin_family = AF_INET;
si.sin_addr.s_addr = inet_addr(_serverip.c_str());
si.sin_port = htons(_serverport);
std::string msg;
while(1)
{
std::cout << "Please input: ";
std::cin >> msg;
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&si, sizeof si);
}
}
private:
uint16_t _serverport;
std::string _serverip;
int _sockfd;
};
// UDPClient.cc
#include "UDPClient.hpp"
#include <memory>
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "incorrect number of parameters" << std::endl;
exit(1);
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
std::unique_ptr<UDPClient> ptr(new UDPClient(ip, port));
ptr->InitClient();
ptr->start();
return 0;
}