理解源IP地址和目的IP地址

IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址

认识端口号

端口号(port)是传输层协议的内容.
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
一个端口号只能被一个进程占用
端口号 vs pid
  • PID(进程 ID):操作系统用来区分软件进程的编号,作用于本机进程
  • 端口号 (Port):操作系统用来区分网络通信通道的编号,作用于网络套接

PID

  1. 每一个运行起来的程序 / 进程,内核分配唯一整型编号
  2. 范围:Linux 默认 1‑32768(现代系统最大可达 400 万 +)
  3. 作用:操作系统管理进程、杀死进程、查看资源占用
  4. 特点:进程重启 PID 大概率改变;只在单台电脑内部生效

端口号 Port

  1. 用来标识一台主机上不同的网络服务
  2. 取值范围:0-65535(16 位无符号整数)
    • 0‑1023:知名端口(HTTP‑80、HTTPS‑443、SSH‑22)
    • 1024‑65535:动态 / 私有端口
  3. 作用:IP 找到主机,端口找到主机里对应的网络程序
  4. 特点:服务可以固定绑定端口;多进程不能同时占用同一个端口
传输层协议(TCPUDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";
基于源ip目的ip源端口目的端口的传输协议就称为socket套接字。

认识TCP协议

传输层协议
有连接
可靠传输
面向字节流

认识UDP协议

传输层协议
无连接
不可靠传输
面向数据报

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

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是一层抽象的网络编程接口,适用于各种底层网络协议,IPv4IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。
IPv4IPv6的地址格式定义在netinet/in.h,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32IP地址.
IPv4IPv6地址类型分别定义为常数AF_INETAF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容. socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
sockaddr 结构
sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.
in_addr结构
in_addr用来表示一个IPv4IP地址. 其实就是一个32位的整数;

简单的UDP网络程序

实现一个简单的英译汉的功能
封装 UdpSocket
udp_socket.hpp
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
class UdpSocket {
public: 
 UdpSocket() : fd_(-1) {
}
 bool Socket() {
 fd_ = socket(AF_INET, SOCK_DGRAM, 0);
 if (fd_ < 0) {
 perror("socket");
 return false;
 }
 return true;
 }
 bool Close() {
 close(fd_);
 return true;
 }
 bool Bind(const std::string& ip, uint16_t port) {
 sockaddr_in addr;
 addr.sin_family = AF_INET;
 addr.sin_addr.s_addr = inet_addr(ip.c_str());
 addr.sin_port = htons(port);
 int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
 if (ret < 0) {
 perror("bind");
 return false;
 }
 return true;
 }
 bool RecvFrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL) {
 char tmp[1024 * 10] = {0};
 sockaddr_in peer;
 socklen_t len = sizeof(peer);
 ssize_t read_size = recvfrom(fd_, tmp,
 sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);
 if (read_size < 0) {
 perror("recvfrom");
 return false;
 }
 // 将读到的缓冲区内容放到输出参数中
 buf->assign(tmp, read_size);
 if (ip != NULL) {
 *ip = inet_ntoa(peer.sin_addr);
 }
 if (port != NULL) {
 *port = ntohs(peer.sin_port);
 }
 return true;
 }
 bool SendTo(const std::string& buf, const std::string& ip, uint16_t port) {
 sockaddr_in addr;
 addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());
 addr.sin_port = htons(port);
 ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr*)&addr, sizeof(addr));
 if (write_size < 0) {
 perror("sendto");
 return false;
 }
 return true;
 }
private:
 int fd_;
};
UDP通用服务器
udp_server.hpp
#pragma once
#include "udp_socket.hpp"
// C 式写法
// typedef void (*Handler)(const std::string& req, std::string* resp);
// C++ 11 式写法, 能够兼容函数指针, 仿函数, 和 lamda
#include <functional>
typedef std::function<void (const std::string&, std::string* resp)> Handler;
class UdpServer {
public:
 UdpServer() {
 assert(sock_.Socket());
 }
 ~UdpServer() {
 sock_.Close();
 }
 bool Start(const std::string& ip, uint16_t port, Handler handler) {
 // 1. 创建 socket
 // 2. 绑定端口号
 bool ret = sock_.Bind(ip, port);
 if (!ret) {
 return false;
 }
 // 3. 进入事件循环
 for (;;) {
 // 4. 尝试读取请求
 std::string req;
 std::string remote_ip;
 uint16_t remote_port = 0;
 bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);
 if (!ret) {
continue;
 }
 std::string resp;
 // 5. 根据请求计算响应
 handler(req, &resp);
 // 6. 返回响应给客户端
 sock_.SendTo(resp, remote_ip, remote_port);
 printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port,
 req.c_str(), resp.c_str());
 }
 sock_.Close();
 return true;
 }
private:
 UdpSocket sock_;
};
实现英译汉服务器
以上代码是对 udp 服务器进行通用接口的封装. 基于以上封装, 实现一个查字典的服务器就很容易了.
dict_server.cc
#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string& req, std::string* resp) {
 auto it = g_dict.find(req);
 if (it == g_dict.end()) {
 *resp = "未查到!";
 return;
 }
 *resp = it->second;
}
int main(int argc, char* argv[]) {
 if (argc != 3) {
 printf("Usage ./dict_server [ip] [port]\n");
 return 1;
 }
 // 1. 数据初始化
 g_dict.insert(std::make_pair("hello", "你好"));
 g_dict.insert(std::make_pair("world", "世界"));
 g_dict.insert(std::make_pair("c++", "最好的编程语言"));
 g_dict.insert(std::make_pair("bit", "特别NB"));
 // 2. 启动服务器
 UdpServer server;
 server.Start(argv[1], atoi(argv[2]), Translate);
 return 0;
}
UDP通用客户端
udp_client.hpp
#pragma once
#include "udp_socket.hpp"
class UdpClient {
public:
 UdpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
 assert(sock_.Socket());
 }
 ~UdpClient() {
 sock_.Close();
 }
 bool RecvFrom(std::string* buf) {
 return sock_.RecvFrom(buf);
 }
 bool SendTo(const std::string& buf) {
 return sock_.SendTo(buf, ip_, port_);
 }
private:
 UdpSocket sock_;
 // 服务器端的 IP 和 端口号
 std::string ip_;
 uint16_t port_;
};
实现英译汉客户端
#include "udp_client.hpp"
#include <iostream>
int main(int argc, char* argv[]) {
 if (argc != 3) {
 printf("Usage ./dict_client [ip] [port]\n");
 return 1;
 }
 UdpClient client(argv[1], atoi(argv[2]));
 for (;;) {
 std::string word;
 std::cout << "请输入您要查的单词: ";
 std::cin >> word;
 if (!std::cin) {
 std::cout << "Good Bye" << std::endl;break;
 }
 client.SendTo(word);
 std::string result;
 client.RecvFrom(&result);
 std::cout << word << " 意思是 " << result << std::endl;
 }
 return 0;
}
地址转换函数
本节只介绍基于IPv4socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
in_addr转字符串的函数
其中inet_ptoninet_ntop不仅可以转换IPv4in_addr,还可以转换IPv6in6_addr,因此函数接口是void *addrptr。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐