预备知识
理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
将数据从主机A发送到主机B;IP唯一标识唯一的主机,主机A是数据发送端也称源IP地址,主机B是数据接受端也称目的IP地址
认识端口号
上述将数据从A主机发送到B主机并不是目的,恰恰只是手段,真正通信的是机器上的软件(进程);IP用来标识主机的唯一,所以端口就是用来标识主机上进程的唯一性
由此,IP地址+该主机上的端口号用来标识该服务器上进程的唯一性
所以,网络通信的本质就是进程间通信
两主机进行通信的前提是需要看到同一份资源–网络资源;通信类似IO,将数据发送出去,读取收到的数据
端口号(port)是传输层协议的内容
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用
理解 “端口号” 和 “进程ID”
进程既然已经有pid为什么还要有port(端口号)呢?
- pid是系统层面的,port是网络层面的;分别设置为了解耦
- 客户需要每次都能够找到进程,服务器的唯一性不能进行改变,恰好ip+port能够满足
- 并不是所有的进程都要提供网络服务,但是所有的进程都需要pid
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”
在网络通信中,ip+port标识唯一性;两主机进行通信时,发送端不仅要发送数据,还要发送一份“多余”的数据也就是自己的ip+port,这份多余的数据便会以协议的形式进行呈现
认识TCP协议
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
在网络通信中上面这些工作已有操作系统给解决;介绍几个接口
#include <arpa/inet.h> //主机转网络 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);
socket编程接口
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);
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议; 然而, 各种网络协议的地址格式并不相同
套接字大致分为三种:网络套接字,原始套接字,Unix域间套接字
若实现编程,则需要三种不同的接口;为了方便使用,只设计一套接口,通过不同的参数,解决所有网络或者其他场景下的通信
结合上面套接字的接口,当进行不同的编程时,使用不同的结构体,只需要进行类型转换;通过前两个字节便可判断是类型
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数
sockaddr 结构
sockaddr_in 结构
协议家族,指明套接字用于通信的类型
端口号
in_port_t sin_port;
ip地址,需要注意的是这里采取的是4字节来表示ip地址;在网络通信中的ip地址形式都是点分十进制,例如"127.0.0.1";所以需要进行类型转换,下面会详细介绍
简单的UDP网络程序
UDP通用服务端
udp服务端初始化
- 创建socket,socket的作用是用来通信的
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;
绑定port,ip;未来服务器要明确的port,不能随意改变;这里就需要上面介绍的sockaddr结构,由于是网络通信所以采用的是sockaddr_in结构;未来服务端给客户端发消息,需要将port和ip要不要发送给对方
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); }
udp服务端启动
服务器的本质其实就是一个死循环
服务端在未来接受客户端传来的数据时,需要知道客户端的端口和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; }; }
UDP通用客户端
客户端时先向服务端发送数据,所以需要提前得知IP和端口
udp客户端初始化
与服务端不同的是,客户端不需要进行显示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; }
udp客户端启动
与服务端不同的是,客户端是先向其发送数据然后再接受其反馈(反馈过程没有展示)
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)); } }
udp客户端完整代码
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);