目录
sendto/recvfrom/send/recv/write/read IO类接口
多进程,多线程,线程池版的基于TCP/IP协议的client-server
针对TCP面向字节流-粘包问题的自定义应用层协议的网络版本计算器server&&client
计算机网络的层状结构
计算机网络是层状的,UDP/TCP协议是传输层协议。
我们使用的网络编程系统调用就是传输层提供的接口。例如accept, connect, listen, socket...
网络传输时,向下要封装报头,向上要解包,也就是去掉报头。发送方需要将应用层数据包逐层向下,添加每层的报头进行封装。接收方通过网络接收到对方传来的报文时,需要逐层向上去掉报头,进行解包,提取出最终的有效载荷。也就是网络发送方真正要传输的数据。
几乎任何协议,都要首先解决两个问题:1. 如何分离(将报头和有效载荷拆分开,接收方需要做的)和如何封装(发送方做的,添加报头)2. 如何向上交付(有效载荷拆分出来之后,交付给上一层)
套接字 = IP + 端口号。IP是网络层协议报头包含的字段,标识着网络传输时应该将数据传输给哪个主机。端口号是传输层协议报头包含的字段,对应着传输层报文中的有效载荷应该交付给该主机上的哪个进程。这样对应进程收到传输层的有效载荷之后,就可以根据应用层协议,将应用层报文中的有效载荷提取出来。
这块其实是网络基础1的内容,之前没写博客... 这里简单记录下。
UDP协议
UDP报文格式
1. 源端口号,目的端口号标明了此UDP报文是哪个进程发出的,发送给哪个进程。
2. 如何解包(分离):UDP采用固定长度报头,接收方将报文前8字节提取出,剩下的就是有效载荷。
3. 如何向上交付:接收方的OS的传输层收到UDP报文之后,16位目的端口号标明了对应进程。(该进程bind了端口号,在内核中,存储诸如port : PCB指针这样的KV类型,就可以通过端口号找到对应的进程)
4. 承接第三点,这也是为什么在应用层编写UDP代码时,定义端口号时,喜欢定义为uint16_t,正是因为传输层协议使用的端口号为16位的。
5. UDP如何提取到整个完整报文:16位UDP长度字段(???????这块好像涉及到UDP报文的存储方式了... 真的需要提取吗....(大概率是需要的...) TCP仅包含报头长度,有什么影响吗???????)
理解UDP/TCP报文的本质
1. UDP/TCP报头在操作系统中本质就是一个位段类型。
2. OS中会有很多UDP报文,TCP报文,那么,OS需要管理这些报文,即先描述,再组织。所以报文在内核中并非仅位段 + 有效载荷。还会有其他字段。
UDP的特点
UDP传输过程类似于寄信。
无连接: 知道对端的IP和端口号就可以直接进行传输, 不需要建立连接;(sendto)
不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该数据段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
面向数据报: 不能够灵活的控制读写数据的次数和数量; : 应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并; 用UDP传输100个字节的数据: 如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节; 发送端的sendto和接收端的recvfrom次数是一样的。
UDP的缓冲区
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区,但是因为UDP不可靠,没有任何传输控制行为。故这个接收缓冲区无法保证接收到的UDP报的顺序和发送UDP报的顺序一致。如果缓冲区满了,再到达的UDP数据就会被丢弃。(提醒一下,接收缓冲区中存储的是UDP报文中去掉报头之后的有效载荷)
sendto/recvfrom/send/recv/write/read IO类接口
我们调用UDP的sendto/recvfrom和TCP的recv/send时,表面上是网络发送和网络接收函数,实质上,它们只是拷贝函数,将应用层缓冲区的数据拷贝到发送缓冲区,将接收缓冲区中的数据拷贝到应用层缓冲区中。(特别是对于TCP而言)(注意,UDP没有发送缓冲区,所以为虚线,若TCP则为实线。)
将数据拷贝到发送缓冲区之后,什么时候进行网络发送,发多少,出错了怎么办,这些都是由传输层协议决定的。缓冲区也是传输层提供的。
UDP是全双工的
UDP没有发送缓冲区,有接收缓冲区,数据在网络中的发送和接收互不影响,可以同时进行,因此为全双工的。UDP的socket既能读,也能写。
UDP注意事项
我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部8字节). 然而64K在当今的互联网环境下, 是一个非常小的数字. 如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装;
UDP协议,实现简单聊天室(服务端+客户端)
udpserver.hpp
#ifndef _UDP_SERVER_HPP_
#define _UDP_SERVER_HPP_
#include "log.hpp"
#include <string>
#include <cstring>
#include <unordered_map>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 基于UDP协议的服务端
class UdpServer
{
public:
UdpServer(uint16_t port, std::string ip = "")
: _port(port), _ip(ip), _sock(-1)
{
}
void initServer()
{
// 1.创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0); // 1.套接字类型:网络套接字(不同主机间通信) 2.面向数据报还是面向字节流:UDP面向数据报
// SOCK_DGRAM支持数据报(固定最大长度的无连接、不可靠消息)。
if (_sock < 0)
{
// 创建套接字失败?
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
// 2.bind : 将用户设置的ip和port在内核中和我们当前的进程强关联
struct sockaddr_in local; // 传给bind的第二个参数,存储ip和port的信息。
local.sin_family = AF_INET;
// 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!-> 故需要转换为网络字节序
local.sin_port = htons(_port); // host->network l 16
// "192.168.110.132" -> 点分十进制字符串风格的IP地址,每一个区域取值范围是[0-255]: 1字节 -> 4个区域,4字节
// INADDR_ANY:让服务器在工作过程中,可以从本机的任意IP中获取数据(一个服务器可能不止一个ip,(这块有些模糊)
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
// 点分十进制字符串风格的IP地址 <-> 4字节整数 4字节主机序列 <-> 网络序列 inet_addr可完成上述工作
if (bind(_sock, (struct sockaddr *)&local, sizeof local) < 0) // !!!
{
logMessage(FATAL, "bind : %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "init udp server %s...", strerror(errno));
}
void start()
{
// 作为一款网络服务器,永远不退出的!-> 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
char buff_message[1024]; // 存储从client发来的数据
for (;;)
{
struct sockaddr_in peer; // 输出型参数
socklen_t len = sizeof peer; // 输出+输入型参数
memset(&peer, 0, len);
// 读取client发来的数据
ssize_t sz = recvfrom(_sock, buff_message, sizeof(buff_message) - 1, 0, (struct sockaddr *)&peer, &len); // 哪个ip/port给你发的
// receive a message from a socket,从一个套接字(或许对应网卡)中接收信息
buff_message[sz] = 0;
uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来,转换为主机序列!哈哈哈
std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列ip->字符串风格的IP
char key[128];
snprintf(key, sizeof key, "%s-%d", cli_ip.c_str(), cli_port);
logMessage(NORMAL, "[%s:%d] clinet worked", cli_ip.c_str(), cli_port);
// printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buff_message); // 可有可无...服务端显示一下客户端发来了什么
if (_map.find(key) == _map.end())
{
_map.insert({key, peer});
logMessage(NORMAL, "[%s:%d] client joined", cli_ip.c_str(), cli_port);
}
// if (sz > 0)
// {
// // 从client获取到了非空数据,client端的ip/port信息存储在peer中。
// buff_message[sz] = 0;
// uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来,转换为主机序列!哈哈哈
// std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列ip->字符串风格的IP
// snprintf(key, sizeof key, "%s-%d", cli_ip.c_str(), cli_port);
// logMessage(NORMAL, "[%s:%d] clinet worked", cli_ip.c_str(), cli_port);
// // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buff_message); // 可有可无...服务端显示一下客户端发来了什么
// if(_map.find(key) == _map.end())
// {
// _map.insert({key, peer});
// logMessage(NORMAL, "[%s:%d] client joined", cli_ip.c_str(), cli_port);
// }
// }
// else
// {
// // 这里的逻辑是:如果client端最初发送空数据,则不加入群聊。
// buff_message[0] = 0;
// }
// 群聊服务端,此时需要给所有群聊中的client,发送某client发来的数据
for (auto &iter : _map)
{
// if(iter.second.sin_port != peer.sin_port)
std::string sendMessage(key);
sendMessage += "# ";
sendMessage += buff_message;
sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&iter.second, sizeof iter.second);
}
}
}
~UdpServer()
{
close(_sock);
}
private:
// 一个服务器,一般必须需要ip地址和port(16位的整数)
uint16_t _port; // 端口号
std::string _ip; // ip
int _sock;
std::unordered_map<std::string, struct sockaddr_in> _map; // [ip:port] | struct
};
#endif
udp_server.cc
#include "udp_server.hpp"
#include <iostream>
#include <memory>
#include <cstdlib>
static void Usage(const char *proc)
{
std::cout << "\nUsage: " << proc << " port\n"
<< std::endl;
}
// 格式:./udp_server 8080
// 疑问: 为什么不需要传ip?
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->initServer();
svr->start();
return 0;
}
udp_client.cc
#include "log.hpp"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
static void Usage(char *proc)
{
std::cout << "\nUsage : " << proc << " server_ip server_port\n"
<< std::endl;
}
void *in(void *args)
{
// 接收server端发送的数据
logMessage(NORMAL, "接收线程已启动");
int sock = *(int *)args; // 套接字
char message_svr[1024]; // 缓冲区
while (true)
{
struct sockaddr_in server;
socklen_t len = sizeof server;
// 一直都在等待UDP报文,进行接收
ssize_t sz = recvfrom(sock, message_svr, sizeof message_svr - 1, 0, (struct sockaddr *)&server, &len);
if (sz > 0)
{
message_svr[sz] = 0;
std::cout << message_svr << std::endl;
}
}
}
struct ThreadData
{
ThreadData(int sock, const std::string &ser_ip, uint16_t ser_port)
: _sock(sock), _server_ip(ser_ip), _server_port(ser_port)
{
}
int _sock;
std::string _server_ip;
uint16_t _server_port;
};
void *out(void *args)
{
// 发送client端发送的数据
logMessage(NORMAL, "发送线程已启动");
ThreadData *td = (ThreadData *)args;
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_port = htons(td->_server_port);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(td->_server_ip.c_str());
std::string message;
while (true)
{
// std::cout << "client# ";
std::cerr << "client# "; // 为了不让这个输出信息也输出到管道中!和服务端发来的进行区分!否则会出现乱码现象。
// fflush(stdout);
std::getline(std::cin, message);
sendto(td->_sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
}
}
// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
logMessage(FATAL, "socket : %d:%s", errno, strerror(errno));
exit(2);
}
pthread_t itid, otid;
pthread_create(&itid, nullptr, in, (void *)&sock);
struct ThreadData td(sock, argv[1], atoi(argv[2]));
pthread_create(&otid, nullptr, out, (void*)&td);
pthread_join(itid, nullptr);
pthread_join(otid, nullptr);
return 0;
}
上方是使用UD