需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)
文章目录
1. 前置知识
1.1 源IP和目的IP
每台主机都有自己的IP地址,当数据在进行通信的时候,除了要发送的数据外,在报头里面还要包含发送方的IP和接收方的IP,这里发送方的IP就被称为源IP,接收方的IP就是目的IP
现在有了源IP和目的IP之后,是不是就能够通信了呢?
源IP和目的IP的出现只能标识两台主机的唯一性,但是在主机上还存在很多个进程,在发送数据的时候肯定是由一个进程发送给另一个进程的,所以还需要标识两台主机上进程的唯一性
为了更好的表示一台主机上服务进程的唯一性,用端口号port标识服务进程、客户端进程的唯一性。
1.2 端口号
端口号(port)是传输层协议的内容
由上面可知:IP地址(主机的全网唯一性)+该主机上的端口号(主机上进程唯一性)可以标识一个唯一的进程。那么最终网络通信的本质就是进程间通信
1.3 TCP协议和UDP协议初识
1. TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
2. UDP协议
UDP(User Datagram Protocol 用户数据报协议
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.4 网络字节序
在之前的文章中数据在内存中的存储,我们提到了数据内存中存储的时候有大端小端之分,磁盘文件中的多字节数据相对于文件中的偏
移地址也有大端小端之分。
大端机器和和小端机器也有可能通信,那么这个时候如何定义网络通信的数据流呢?
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
#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);
实际上这些函数名是很好记的,按照作用来组合即可,其中h表示host,是当前主机的意思;n表示network,是网络的意思;l表示32位长整数,s表示16位短整数
例如:htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
1.5 socket编程接口
1.5.1 socket常见API
// 创建 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);
1.5.2 sockaddr结构
套接字不仅支持跨网络通信,还支持本地的进程间通信(域间套接字)。对于不同的通信方式,需要使用的接口在细节上是有一些不同的。所以套接字提供了两个结构体sockaddr_in
和sockaddr_un
结构体,其中前者是用于跨网络通信的,后者用于本地进程间通信。但是除此之外,通信的方法基本都是相同的,所以为了让两种通信方式能使用同一套函数接口,所以就定义了sockaddr
结构体,该结构体与sockaddr_in
和sockaddr_un
的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。
三个结构体的内容如下:
所以当我们在传参的时候,就直接传sockaddr结构体,在函数内部做识别,执行对应的操作。
注意: 实际我们在进行网络通信时,定义的还是sockaddr_in
这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr
*罢了。
2. 简单的UDP服务端和客户端代码实现
这里我们采用C和C++混编的方式,就封装成类来实现,通过makefile进行自动化编译。
2.1 makefile文件的编写
# 这是构建本项目的makefile文件
cc=g++ # 这里cc是一个变量,表示构建项目使用的编译器
.PHONY:all # 构建一个伪对象,表示我们要同时构建两个目标文件
all: udpServer udpClient
# 这里是两个目标文件的依赖和构建方法
udpServer:udpServer.cc
$(cc) -o $@ $^ -std=c++11
udpClient:udpClient.cc
$(cc) -o $@ $^ -std=c++11
# 删除所有产生的目标文件
.PHONY:clean
clean:
rm -f udpServer udpClient
2.2 server端的编写
2.2.1 需要调用的函数
1. socket
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int socket(int domain, int type, int protocol);
参数解释:
domain:表示通信类型(本质是一个宏),socket支持多种通信方式,一般来说有本地和网络两种。AF_INET表示使用IPv4进行网络通信;AF_UNIX表示本地通信。详情见man手册
type:表示套接字提供服务的类型,如SOCK_STREAM:流式服务(TCP策略);SOCK_DGRAM:数据报服务(UDP策略)
protocol:表示对应的协议,实际上可以由前两个参数确定,所以这里设计成0没有问题
函数描述:
创建一个用于网络通信的文件描述符
返回值:
调用成功返回文件描述符,否则返回-1同时设置错误码
2. bind
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解释:
sockfd:需要设置的sockfd
addr:将需要设置的内容对应的结构体指针强转为struct sockaddr *再传入(为了统一接口)
addrlen:传入的addr对应的变量大小
函数描述:
将local设置到内核中。
返回值:
调用成功返回0,否则返回-1,同时设置错误码
3. bzero
头文件:
#include <strings.h>
函数原型:
void bzero(void *s, size_t n);
参数解释:
s:需要设置的变量的地址
n:变量的长度
函数描述:
从s开始的n个字节设置为0,类似于memset
4. inet_aton
头文件:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
函数原型与描述:
int inet_aton(const char *cp, struct in_addr *inp); // 将点分十进制的IP地址转换为符合网络字节序的二进制形式存放到in_addr结构体中
参数解释:
cp:点分十进制形式的C式ip字符串
inp:存放网络字节序的二进制i形式IP
返回值:
如果地址有效,则返回非0,如果地址无效则返回0
5. recvfrom
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int falgs, struct sockaddr *src_addr, socklen_t *addrlen);
函数描述:
接收信息
参数解释:
sockfd:用于接收的sockfd
buf:
len:
flags:读取的方式,默认为0,阻塞读取
src_addr:收到消息除了本身,还得知道是谁发的,输入输出型参数,返回对应的消息内容是从哪一个client发来的
addrlen:结构体大小
返回值:
返回接收到的字节数,出错返回-1同时设置错误码。如果发送方正常关闭,返回0
2.2.2 代码
/*udpServer.hpp*/
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <string.h>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
namespace Server
{
using func_t = std::function<void(std::string, uint16_t, std::string)>;
static void Usage()
{
std::cout << "\nUsage:\n\t./udpServer local_port\n\n";
}
enum // 枚举出错类型
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
const static std::string defaultIP = "0.0.0.0";
const int gnum = 1024; // 处理任务的缓冲区大小
class udpServer
{
public:
udpServer(const func_t &cb, const uint16_t &port, const std::string &ip = defaultIP)
: _port(port), _ip(ip) {}
// 这里初始化要做的事情有两件: 1. 创建sockfd 2.bind端口号和ip
void initServer()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 参数1:这里使用AF_INET表示使用IPv4进行网络通信;参数2:我们这里使用UDP策略;参数3:这里使用0表示默认
if (_sockfd == -1) // 差错处理
{
std::cerr << "socket error " << errno << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "socket sucess : " << _sockfd << std::endl;
// 在当前函数的栈帧上创建一个local对象,设置相关属性,然后将相关属性bind到系统内核中
struct sockaddr_in local; // 这里struct sockaddr_in类型需要头文件arpa/inet.h
bzero(&local, sizeof(local)); // 在填充数据之前首先将对象内部元素清空,这里使用bzero
local.sin_family = AF_INET; // 设定协议家族
local.sin_port = htons(_port); // 设置端口号,这里端口号需要首先转换成网络端口号
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 设置ip,这里的ip是string类型,但是实际在传输的时候使用的是整型,所以需要转换,这里使用inet_addr
// inet_addr的作用有两个: 1.string -> uint32_t; 2. htonl()
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 将local设置到内核中,即bind
if (n == -1)
{
std::cerr << "bind error " << errno << strerror(errno) << std::endl;
exit(BIND_ERR);
}
// 至此初始化的操作完成
}
void start() // 让服务器开始跑起来
{
// 服务器的本质是一个死循环,在循环内部处理收到的任务
char buffer[gnum];
while (true)
{
// 1. 读取数据
struct sockaddr_in peer; // 定义一个变量用于接收数据
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
// a. 数据是什么 b. 谁发的
if (n > 0)
{
buffer[n] = 0;
std::string clientIp = inet_ntoa(peer.sin_addr); // 转换网络字节序, 点分十进制
uint16_t clientPort = ntohs(peer.sin_port);
std::string message = buffer;
std::cout << clientIp << "[" << clientPort << "]# " << message << std::endl;
// 2. 处理任务
_callback(clientIp, clientPort, message);
}
}
}
private:
// 成员变量分析:作为一个服务端进程,我们首先需要一个端口号port和一个本地ip
// 还需要有一个文件描述符sockfd,用于进行通信(网络通信是基于文件的,所以使用的都是文件的一套内容,包括fd)
int _sockfd; // socket文件描述符
std::string _ip; // 本地ip
uint16_t _port; // 服务进程端口号
func_t _callback;
};
}
2.2.3 测试
那么本地测试没有问题,这里使用的是云服务器,能不能bind公网IP呢?
./udpServer 8080 公网IP
云服务器是虚拟化的服务器,不能直接bind你的公网IP,可以绑定内网IP(ifconfig显示的);如果是虚拟机或者独立真实的Linux环境,你可以bind你的IP;
-
如何保证云服务器能够被别人访问?
实际上,一款网络服务器不建议指明一个IP,也就是不要显示地绑定IP。服务器IP可能不止一个,如果只绑定一个明确的IP,最终的数据可能用别的IP来访问端口号,访问不出来,所以真实的服务器IP一般采用INADDR_ANY(全0,任意地址)代表任意地址bind
最终代码:
/*udpServer.hpp*/
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <string.h>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
namespace Server
{
using func_t = std::function<void(std::string, uint16_t, std::string)>;
static void Usage()
{
std::cout << "\nUsage:\n\t./udpServer local_port\n\n";
}
enum // 枚举出错类型
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
const static std::string defaultIP = "0.0.0.0";
const int gnum = 1024; // 处理任务的缓冲区大小
class udpServer
{
public:
udpServer(const func_t &cb, const uint16_t &port, const std::string &ip = defaultIP)
: _port(port), _ip(ip) {}
// 这里初始化要做的事情有两件: 1. 创建sockfd 2.bind端口号和ip
void initServer()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 参数1:这里使用AF_INET表示使用IPv4进行网络通信;参数2:我们这里使用UDP策略;参数3:这里使用0表示默认
if (_sockfd == -1) // 差错处理
{
std::cerr << "socket error " << errno << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "socket sucess : " << _sockfd << std::endl;
// 在当前函数的栈帧上创建一个local对象,设置相关属性,然后将相关属性bind到系统内核中
struct sockaddr_in local; // 这里struct sockaddr_in类型需要头文件arpa/inet.h
bzero(&local, sizeof(local)); // 在填充数据之前首先将对象内部元素清空,这里使用bzero
local.sin_family = AF_INET; // 设定协议家族
local.sin_port = htons(_port); // 设置端口号,这里端口号需要首先转换成网络端口号
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 设置ip,这里的ip是string类型,但是实际在传输的时候使用的是整型,所以需要转换,这里使用inet_addr
local.sin_addr.s_addr = INADDR_ANY;
// inet_addr的作用有两个: 1.string -> uint32_t; 2. htonl()
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 将local设置到内核中,即bind
if (n == -1)
{
std::cerr << "bind error " << errno << strerror(errno) << std::endl;
exit(BIND_ERR);
}
// 至此初始化的操作完成
}
void start() // 让服务器开始跑起来
{
// 服务器的本质是一个死循环,在循环内部处理收到的任务
char buffer[gnum];
while (true)
{
// 1. 读取数据
struct sockaddr_in peer; // 定义一个变量用于接收数据
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
// a. 数据是什么 b. 谁发的
if (n > 0)
{
buffer[n] = 0;
std::string clientIp = inet_ntoa(peer.sin_addr); // 转换网络字节序, 点分十进制
uint16_t clientPort = ntohs(peer.sin_port);
std::string message = buffer;
std::cout << clientIp << "[" << clientPort << "]# " << message << std::endl;
// 2. 处理任务
_callback(clientIp, clientPort, message);
}
}
}
private:
// 成员变量分析:作为一个服务端进程,我们首先需要一个端口号port和一个本地ip
// 还需要有一个文件描述符sockfd,用于进行通信(网络通信是基于文件的,所以使用的都是文件的一套内容,包括fd)
int _sockfd; // socket文件描述符
std::string _ip; // 本地ip
uint16_t _port; // 服务进程端口号
func_t _callback;
};
}
/*udpServer.cc*/
#include "udpServer.hpp"
#include <memory>
using namespace Server;
void handleMessage(std::string clientIp, uint16_t clientPort, std::string message)
{
std::cout << "我是一个回调函数执行,收到的消息是:" << message << std::endl;
}
// 调用的指令 :./udpServer port
int main(int argc, char *argv[])
{
// 解析指令
if (argc != 2)
{
Usage();
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
// 创建对象,进行通信
std::unique_ptr<udpServer> usvr(new udpServer(handleMessage, port));
usvr->initServer(); // 初始化服务进程
usvr->start(); // 开始监听
return 0;
}
2.3 client端的编写
2.3.1 需要调用的函数
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
ssize_t sendto(int sockfd, void *buf, size_t len, int falgs, struct sockaddr *dest_addr, socklen_t *addrlen);
函数描述:
向指定IP和端口号的进程发送信息
参数解释:
sockfd:用于发送的sockfd
buf:要发送的数据的地址
len:要发送的数据长度
flags:发送的方式,默认为0,阻塞发送
dest_addr:接收方的IP和端口号
addrlen:结构体大小
返回值:
调用成功返回接发送的字节数,出错返回-1同时设置错误码
2.3.2 代码
/*udpCliet.hpp*/
#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
namespace Client
{
enum // 枚举出错类型
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
};
class udpClient
{
public:
udpClient(const std::string &serverIp, const uint16_t &serverPort)
: _serverIp(serverIp), _serverPort(serverPort), _sockfd(-1), _quit(false) {}
void initClient()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
std::cerr << "socket error: " << errno << " : " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "socket success: " << _sockfd << std::endl;
// 2. bind
// a. Client要不要bind -- 当然要
// b. Client要不要程序员显示bind -- 不用,由OS自动形成端口进行bind。因为Client是在客户机上运行的,客户机上同时会有很多其他的Client在跑,不能确定哪些端口已经被使用
// c. OS在什么时候,如何bind -- 在Client发起网络连接时自动执行bind操作
}
void run()
{
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);
std::string message;
while(!_quit)
{
std::cout << "Please Enter# ";
std::cin >> message;
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
}
private:
int _sockfd; // 套接字
std::string _serverIp; // 服务端IP
uint16_t _serverPort; // 服务端端口号
bool _quit; // 客户端退出标志
};
} // namespace Client
/*udpCliet.cc*/
#include "udpClient.hpp"
#include <string>
#include <memory>
using namespace Client;
static void Usage(std::string proc)
{
std::cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n";
}
// ./udpClient server_ip server_port
int main(int argc, char *argv[])
{
// 解析指令
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
std::unique_ptr<udpClient> ucli(new udpClient(serverIp, serverPort));
ucli->initClient();
ucli->run();
return 0;
}
2.3.3 测试
./udpClient 127.0.0.1 18989 # 客户端启动
./udpServer 18989 # 服务端启动
本节完…