在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
将数据从主机A发送到主机B;IP唯一标识唯一的主机,主机A是数据发送端也称源IP地址,主机B是数据接受端也称目的IP地址
上述将数据从A主机发送到B主机并不是目的,恰恰只是手段,真正通信的是机器上的软件(进程);IP用来标识主机的唯一,所以端口就是用来标识主机上进程的唯一性
由此,IP地址+该主机上的端口号用来标识该服务器上进程的唯一性
所以,网络通信的本质就是进程间通信
两主机进行通信的前提是需要看到同一份资源–网络资源;通信类似IO,将数据发送出去,读取收到的数据
端口号(port)是传输层协议的内容
进程既然已经有pid为什么还要有port(端口号)呢?
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”
在网络通信中,ip+port标识唯一性;两主机进行通信时,发送端不仅要发送数据,还要发送一份“多余”的数据也就是自己的ip+port,这份多余的数据便会以协议的形式进行呈现
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
在网络通信中上面这些工作已有操作系统给解决;介绍几个接口
#include
//主机转网络 long long
uint32_t htonl(uint32_t hostlong);
//主机转网络 short
uint16_t htons(uint16_t hostshort);
//网络转主机 long long
uint32_t ntohl(uint32_t netlong);
//网络转主机 short
uint16_t ntohs(uint16_t netshort);
int socket(int domain, int type, int protocol);
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
socket API是一层抽象的网络编程接口,适用于各种底层网络协议; 然而, 各种网络协议的地址格式并不相同
套接字大致分为三种:网络套接字,原始套接字,Unix域间套接字
若实现编程,则需要三种不同的接口;为了方便使用,只设计一套接口,通过不同的参数,解决所有网络或者其他场景下的通信
结合上面套接字的接口,当进行不同的编程时,使用不同的结构体,只需要进行类型转换;通过前两个字节便可判断是类型
端口号
in_port_t sin_port;
ip地址,需要注意的是这里采取的是4字节来表示ip地址;在网络通信中的ip地址形式都是点分十进制,例如"127.0.0.1";所以需要进行类型转换,下面会详细介绍
int socket(int domain, int type, int protocol);
AF_INET
协议表明此socket是网络通信
SOCK_DGRAM
表明通信的数据形式是数据报的类型
需要注意的是,创建socket成功返回的是一个文件描述符,所以udp通信的本质其实就是进程间通信
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
struct sockaddr_in local;
bzero(&local, sizeof(local));
这里的协议与上面不同,这里的协议是表明填充sockaddr_in结构用于网络通信
local.sin_family = AF_INET;
在前面介绍过在网络中数据统一是大端存储,这里需要调用API对数据进行转换
local.sin_port = htons(_port);
网络中的IP地址形式是点分十进制,此结构中采用的是4字节来表示,所以需要将IP数据类型转换为4字节,然后还需要转换为大端;同理这里也是采取API
local.sin_addr.s_addr = inet_addr(_ip.c_str());
一般服务端不会指定特定IP,因为一个服务器的一个端口会接受许多不同IP客户端传来的数据;如果指定IP,会导致其余客户端的数据丢失,所以一般使用"0.0.0.0"用来接受所有数据(同一端口)
local.sin_addr.s_addr = htonl(INADDR_ANY);
以上操作只是在用户栈上进行的,操作系统并不知晓,所以需要进行bind
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
在传入数据时,需要进行类型转换以实现统一接口
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n == -1)
{
cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
exit(BIND_ERR);
}
服务器的本质其实就是一个死循环
服务端在未来接受客户端传来的数据时,需要知道客户端的端口和IP地址,这些数据就是保存在sockaddr_in结构中的,接受的过程这些数据是由操作系统自动进行填写;通信是双方的,既然服务端接受到了数据,所以也需要做出回应,回应的过程操作类似,这里没有进行展示
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
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 << clientip << "[" << clientport << "]# " << message << endl;
}
}
}
namespace Server
{
using namespace std;
static const string defaultIp = "0.0.0.0"; // TODO
static const int gnum = 1024;
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
typedef function<void(string, uint16_t, string)> func_t;
class udpServer
{
public:
udpServer(const uint16_t &port, const string &ip = defaultIp)
: _port(port), _ip(ip), _sockfd(-1)
{
}
void initServer()
{
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(SOCKET_ERR);
}
cout << "socket success: "
<< " : " << _sockfd << endl;
// 2. 绑定port,ip(TODO)
// 未来服务器要明确的port,不能随意改变
struct sockaddr_in local; // 定义了一个变量,栈,用户
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 你如果要给别人发消息,你的port和ip要不要发送给对方
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->uint32_t 2. htonl(); -> inet_addr
// local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind,服务器的真实写法
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n == -1)
{
cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
exit(BIND_ERR);
}
// UDP Server 的预备工作完成
}
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 << clientip << "[" << clientport << "]# " << message << endl;
}
}
}
~udpServer()
{
}
private:
uint16_t _port;
string _ip; // 实际上,一款网络服务器,不建议指明一个IP
int _sockfd;
};
}
客户端时先向服务端发送数据,所以需要提前得知IP和端口
与服务端不同的是,客户端不需要进行显示bind;因为服务端只有一个,而客户端却是多个,在客户端第一次向服务端发送数据时,操作系统会自动对其进行bind,填入必要的数据:端口和IP
void initClient()
{
// 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(2);
}
cout << "socket success: "
<< " : " << _sockfd << endl;
}
与服务端不同的是,客户端是先向其发送数据然后再接受其反馈(反馈过程没有展示)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
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);
string message;
while (!_quit)
{
cout << "Please Enter# ";
cin >> message;
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
}
namespace Client
{
using namespace std;
class udpClient
{
public:
udpClient(const string &serverip, const uint16_t &serverport)
: _serverip(serverip), _serverport(serverport), _sockfd(-1), _quit(false)
{
}
void initClient()
{
// 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(2);
}
cout << "socket success: "
<< " : " << _sockfd << endl;
}
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);
string message;
while (!_quit)
{
cout << "Please Enter# ";
cin >> message;
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
}
~udpClient()
{
}
private:
int _sockfd;
string _serverip;
uint16_t _serverport;
bool _quit;
};
}
sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换
字符串转in_addr的函数:
in_addr_t inet_addr(const char *cp);
in_addr转字符串的函数:
char *inet_ntoa(struct in_addr in);
TCP与UDP的区别:是否需要链接,通信的数据类型也不同
TCP需要链接,通信数据是面向字节流
与udp服务端不同的是,tcp服务需要先进行链接
举个栗子:
当你向客服进行询问,前提是客服随时都在线,即使没有客户进行询问时也必须在线,将这种状态称为“监听”;tcp服务端也是如此,在进行链接之前需要将socket进行监听(第二个参数后面会介绍)
int listen(int sockfd, int backlog);
// 3.设置socket为监听状态
if (listen(_listensock, gbacklog) < 0)
{
std::cerr << "listen socket error" << std::endl;
exit(LISTEN_ERR);
}
std::cout << "listen socket success!" << std::endl;
接下来就是建立链接的过程,再举个栗子
在赌场周围会有拉客的人,称他们为张三;他们的目的就是拉客,将你拉入赌场进行消费,而当你进入赌场之后,真正为你服务的却不是那群拉客的人,而是里面的工作人员,称作李四;这里的张三的作用就只用来拉客,李四才是真的服务人员;tcp服务端也是如此,进行监听的socket在链接成功之后,会返回一个新的socket,新生成的socket的作用才是用来通信的;由于tcp通信是面向字节流,所以在链接成功之后,接下来的通信本质其实就是文件操作(IO操作)
int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
// 4.server获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
std::cerr << "accept error,next" << errno << ":" << strerror(errno) << std::endl;
continue;
}
std::cout << "accept success" << std::endl;
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const uint16_t gport = 8080;
static const int gbacklog = 5;
class TcpServer;
class ThreadData
{
public:
ThreadData(TcpServer *self, int sock)
: _self(self), _sock(sock)
{
}
public:
TcpServer *_self;
int _sock;
};
class TcpServer
{
public:
TcpServer(const uint16_t &port = gport)
: _listensock(-1), _port(port)
{
}
~TcpServer()
{
}
void InitServer()
{
// 1.创建socket文件套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
std::cerr << "create socket error" << errno << ":" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success!" << std::endl;
// 2.bind绑定自己的网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind socket error" << errno << ":" << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success!" << std::endl;
// 3.设置socket为监听状态
if (listen(_listensock, gbacklog) < 0)
{
std::cerr << "listen socket error" << std::endl;
exit(LISTEN_ERR);
}
std::cout << "listen socket success!" << std::endl;
}
void start()
{
for (;;)
{
// 4.server获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
std::cerr << "accept error,next" << errno << ":" << strerror(errno) << std::endl;
continue;
}
std::cout << "accept success" << std::endl;
// 多线程版
pthread_t tid;
ThreadData *td = new ThreadData(this, sock);
pthread_create(&tid, nullptr, thread_routine, td);
}
}
static void *thread_routine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->_self->serviceIO(td->_sock);
close(td->_sock);
delete td;
return nullptr;
}
void serviceIO(int sock)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前为止将读到的数据当成字符串
buffer[n] = 0;
std::cout << "read message:" << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += "server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size());
}
else if (n == 0)
{
// 数据被读取完毕,客户端退出
std::cout << "client quit,me too!" << std::endl;
break;
}
}
close(sock);
}
private:
int _listensock;
uint16_t _port;
};
}
这个过程与udp一样,不加赘述
void Initclient()
{
// 1.创建socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
std::cerr << "socket create error" << errno << strerror(errno) << std::endl;
exit(1);
}
}
既然服务端是监听,那么之后客服端发起链接之后,二者才能进行链接,然后通信
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "socket connect error" << std::endl;
}
else
{
std::string msg;
while (true)
{
std::cout << "Enter#";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前为止将读取的数据当成字符串
buffer[n] = 0;
std::cout << "Server回显#" << buffer << std::endl;
}
else
{
break;
}
}
}
#define NUM 1024
namespace client
{
class TcpClient
{
public:
TcpClient(const std::string &serverip, const uint16_t &serverport)
: _sock(-1), _serverip(serverip), _serverport(serverport)
{
}
void Initclient()
{
// 1.创建socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
std::cerr << "socket create error" << errno << strerror(errno) << std::endl;
exit(1);
}
}
void start()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "socket connect error" << std::endl;
}
else
{
std::string msg;
while (true)
{
std::cout << "Enter#";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前为止将读取的数据当成字符串
buffer[n] = 0;
std::cout << "Server回显#" << buffer << std::endl;
}
else
{
break;
}
}
}
}
~TcpClient()
{
if (_sock >= 0)
close(_sock);
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
}
这里还存在一个问题,服务器运行之后,之前的任何指令都会被当做消息被发送,不会再被命令行解释器执行;通过键盘可直接将其停止;真正的服务器应该是保存在云端,一直运行不会受其他因素影响
接下来接受守护进程来解决这个问题
xshell链接远端服务器之后,服务器立刻形成一个会话:有且只有一个前台任务,多个后台任务;其中bash命令行解释器一般作为前台任务,用来执行各种指令
通过命令行解释器形成两个后台任务;仔细观察会发现:两个任务分别是由两个组长(31271,31416)带领的,PGID为组长的代号;所有成员的共同领导是前台bash解释器(30990)
如果将其中一个后台任务,转换到前台结果会怎么样呢?
由于Bash命令行解释器被切换为了后台,所以各种指令任务一都无法执行;和上面的情形一致,守护进程是自称会话,相当于自己既是领导也是组长同时也是员工
模拟实现守护进程
1.使调用进程忽略异常的信号
signal(SIGPIPE, SIG_IGN);
如何服务端出现异常,客服端向其发送消息时会直接忽略掉异常,可以继续发送
2.只有组员才能自成会话,setsid
举个栗子,在公司里面组长不允许直接离职创业,因为他需要管理下面的员工,但是员工就可以直接离职创业;这里采取的方式是,父进程退出,子进程自成会话
if (fork() > 0)
exit(1);
// 子进程-》守护进程 本质也是孤儿进程的一种
pid_t id = setsid();
assert(id != -1);
3.守护进程是脱离终端的,关闭或者重定向之前进程默认打开的文件
进程默认会打开0,1,2文件描述符所指向的文件,为确保服务器不受器影响,需要将其关闭;这里采取的是将其重定向到”文件黑洞“,可以接受所有指令
int fd = open(DEV, O_RDWR);
if (fd >= 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
}
else
{
close(0);
close(1);
close(2);
}
完整代码
#define DEV "/dev/null"
void deamonSelf()
{
// 1.使调用进程忽略异常的信号
signal(SIGPIPE, SIG_IGN);
// 2.只有组员才能自成会话,setsid
if (fork() > 0)
exit(1);
// 子进程-》守护进程 本质也是孤儿进程的一种
pid_t id = setsid();
assert(id != -1);
// 3.守护进程是脱离终端的,关闭或者重定向之前进程默认打开的文件
int fd = open(DEV, O_RDWR);
if (fd >= 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
}
else
{
close(0);
close(1);
close(2);
}
}
服务端运行之后,确实立刻自成会话
查看服务端进程,被1号进程领养,自此网络通信告一段落
建立连接的过程:
这个建立连接的过程, 通常称为 三次握手;
数据传输的过程:
断开连接的过程:
这个断开连接的过程, 通常称为 四次挥手