网络协议分析-重点总结

发布时间 2023-12-21 03:47:55作者: lrui1

第一章 计算机网络基础

1.1 网络的概念和组成

连接到Internet的所有的这些设备称为主机终端系统

终端系统由通信链接连在一起。常见的通信链接有双绞线同轴电缆光纤,负责传输原始的比特流

不是单一通过通信链接连接,通过中介交换设备间接相连,这些中介交换设备称为包交换器,传输的信息块称为,当今Internet上最基本的两种是交换机路由器

ISP——Internet Service Provider
TCP——Transfer Control Protocol
IP——Internet Protocol

还有许多专用网络,这些网络称为企业内部互联网

1.2 计算机网络参考模型

现代网络通信结构以层划分,各层和各层协议的集合称为网络体系,特定系统使用的一组协议称为协议堆栈

OSI模型 重点

1.3 网络程序寻址方式

存在竞争信道时,解决此问题的协议是MAC(Medium Access Control) 子层

IP地址的分类

IP地址根据网络号和主机号来分,分为A、B、C三类及特殊地址D、E。

全0和全1的都保留不用。
全0——本网络和主机,全1——广播地址指定网络中所有主机

在IP地址3种主要类型里,各保留了3个区域作为私有地址,其地址范围如下:

A类地址:10.0.0.0~10.255.255.255

B类地址:172.16.0.0~172.31.255.255

C类地址:192.168.0.0~192.168.255.255

第二章 Winsock编程接口

2.1 Winsock 库

Winsock库有两个版本,目前使用Winsock2
使用时

  1. 包含头文件winsock2.h
  2. 添加WS2_32.lib 库的链接

封装CInitSock 类

#include <winsock2.h>
#pragma comment(lib, "WS2_32")

class CInitSock
{
public:
	CInitSock(BYTE minorVer=2, BYTE majorVer = 2)
	{
		WSADATA wsaData;
		WORD sockVersion = MAKEWORD(minorVer, majorVer)
		if(::WSAStartup(sockVersion, &wsaData) != 0)
		{
			exit(0);
		}
	}
	~CInitSock()
	{
		::WSACleanup();
	}
}
  • 构造函数 CInitSock(BYTE minorVer = 2, BYTE majorVer = 2):这是类的构造函数,它接受两个参数,分别是用于指定Winsock版本的minor版本号和major版本号。如果未提供这些参数,默认使用版本号2.0。构造函数首先声明一个WSADATA结构,该结构用于接收Winsock初始化信息。然后,使用MAKEWORD宏将minor和major版本号组合成一个WORD类型的版本号。接着,调用WSAStartup函数初始化Winsock。如果初始化失败,程序调用exit(0)来退出程序。

  • 析构函数 ~CInitSock():这是类的析构函数,用于在对象生命周期结束时清理Winsock资源。在这里,它调用WSACleanup函数来释放Winsock占用的资源。

2.2 Winsock的寻址方式和字节顺序

2.2.1 Winsock 寻址

Winsock要兼容多个协议,因此它有采取通用的地址结构sockaddr

struct sockaddr
{
	u_short sa_family; // 指定地址的家族
	char sa_data[14]; // 对应家族的地址格式
}

Winsock已经定义了sockaddr结构的TCP/IP 版本 —— sockaddr_in 结构

struct sockaddr_in
{
	short sin_family; // 地址家族,应为AF_INET
	u_short sin_port; // 端口号
	struct in_addr sin_addr; // ip 地址
	char sin_zero[8]; //空字节,要设为0
}

端口号可分为下面三个范围

  • 0~1023 由IANA管理,保留为公共的服务使用
  • 1024~49151 普通用户注册的端口号
  • 49152~65535 动态和/或 私有的端口号
    普通用户的应用程序选择1024~49151 当然 49152~65535也可以

sin_addr 存储IP地址 是一个联合,如下

struct in_addr
{
	union
	{
		struct { u_char s_b1, s_b2, s_b3, s_b4; } S_un_b; // 以4个U_char 描述
		struct { u_short s_w1, s_w2; } S_un_w; // 以2个u_short描述
		u_long S_addr; // 以1个u_long 描述
	}
}

IP地址转换:应用程序可以使用inet_addr函数将一个点分十进制格式的IP地址字符串转换成32位二进制数表示的IP地址;inet_ntoainet_addr的逆函数,可以将IP的32位二进制格式转换为字符串格式。inet_addr返回的二进制是以网络顺序存储的(大尾)

2.2.2 字节顺序

TCP/IP统一采用大尾方式传输,也称为网络字节顺序

2.3 Winsock 编程详解

2.3.1 Winsock编程流程

1、套接字的创建和关闭

使用套接字之前,需调用socket函数创建一个套接字对象

SOCKET socket(
	int af, // 指定套接字格式,Winsock只支持AF_INET
	int type, // 指定套接字类型
	int protocol // 配合type参数使用,指定协议类型
)

type常见参数

  • SOCK_STREAM: 流式套接字,TCP
  • SOCK_DGRAM: 数据包套接字,UDP
  • SOCK_RAW: 原始套接字,Winsock不处理,程序自行处理数据包及协议首部

2.3.2 典型过程图

2.3.4 UDP编程

1、UDP编程流程

  1. 服务器端程序设计流程如下
    1. 创建套接字-socket
    2. 绑定IP地址和端口-bind
    3. 收发数据-sendto/recvfrom
    4. 关闭连接-closesocket
  2. 客户端程序设计流程如下
    1. 创建套接字-socket
    2. 收发数据-sendto/recvfrom
    3. 关闭连接-closesocket

第三章 Windows套接字 I/O 模型

Winsock提供了一些 I/O 模型帮助应用程序以异步方式在一个或多个套接字上管理 I/O,这样的I/O有6种:阻塞(blocking)、选择(select)、异步选择(WSAAsyncSelect)、事件通知(WSAEventSelect)、重叠(overlapped)、完成端口(completion port)模型-不考

3.1 套接字模式

Winsock以两种模式执行I/O操作:阻塞和非阻塞

3.1.1 阻塞模式

套接字创建时,默认的工作模式

3.1.2 非阻塞模式

3.2 选择(Select)模型

select模型,使用select函数管理I/O,目的是允许应用程序有能力管理多个套接字,让套接字调用的程序是阻塞的,非阻塞的都行

3.2.1 select函数

int select(
	int nfds, // 忽略 作兼容性用
	fd_set* readfds, // 指向一个套接字集合,检查可读性
	fd_set* writefds, // 指向一个套接字集合,检查可写性
	fd_set* exceptfds, //指向一个套接字集合,用来检查错误
	const struct timeval* timeout // 指定此函数等待的最长时间,结果为NULL-无限制
);

函数调用成功,返回发送网络事件的所有套接字数量的总和,超出时间限制返回0,失败返回SOCKET_ERROR

1、套接字集合

fd_set 结构如下
typedef struct fd_set {
u_int fd_count; // 下面数组的大小
SOCKET fd_array[FD_SETSIZE]; // 套接字句柄数组
}fd_set;

下面是Winsock定义的4个操作fd_set 套接字集合的宏

FD_ZERO(*set); // 初始化set为空集合
FD_CLR(s, *set); //从set集合移除套接字s
FD_ISSET(s, *set); // 检查s是否是set集合的成员,是返回TRUE
FD_SET(s, *set); // 添加套接字到集合中

2、网络事件

传递给select函数的三个fds集合中的套接字被标识的条件

  1. readfds集合:
    • 数据可读
    • 连接已经关闭,重启或中断
    • 如果listen已经被调用,并且有一个连接未决,accept函数将成功
  2. writefds集合:
    • 数据能够发送
    • 如果一个非阻塞连接调用正在被处理,连接已经成功
  3. exceptfds集合:
    • 如果一个非阻塞连接调用正在被处理,连接失败
    • OOB数据可读

测试套接字s是否可读,添加到readfds集合,等待select函数返回,还在说明可读了

3.2.2 应用举例

#include <stdio.h>
#include "initsock.h"

CInitSock c;

int main()
{
    USHORT nPort = 4567;
    // create socket ip
    SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(nPort);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;
    // bind
    if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        printf(" Failed bind() \n");
		return -1;
    }

    // listen
    ::listen(sListen, 5);

    // select 工作过程
    fd_set fdSocket; //所有可用套接字集合
    FD_ZERO(&fdSocket);
    FD_SET(sListen, &fdSocket);
    while(TRUE)
    {
        fd_set fdRead = fdSocket;
        int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
        if(nRet > 0)
        {
            for(int i = 0; i < (int)fdSocket.fd_count; i++)
            {
                if(FD_ISSET(fdSocket.fd_array[i], &fdRead))
                {
                    if(fdSocket.fd_array[i] == sListen)
                    {
                        if(fdSocket.fd_count < FD_SETSIZE)
                        {
                            sockaddr_in addrRemote;
                            int nAddrLen = sizeof(addrRemote);
                            SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
                            FD_SET(sNew, &fdSocket);
                            printf("接收到连接(%s)\n", ::inet_ntoa(addrRemote.sin_addr));
                        }
                        else
                        {
                            printf(" Too much connections! \n");
                            continue;
                        }
                    }
                    else
                    {
                        char szText[256];
                        int nRecv = ::recv(fdSocket.fd_array[i], szText, sizeof(szText), 0); // strlen 不行,得sizeof 
                        if(nRecv > 0)
                        {
                            szText[nRecv] = '\0';
                            printf("接收到数据: %s \n", szText);
                        }
                        else
                        {
                            ::closesocket(fdSocket.fd_array[i]);
                            FD_CLR(fdSocket.fd_array[i], &fdSocket);
                        }
                    }
                }
            }
        }
        else
		{
			printf(" Failed select() \n");
			break;
		}
    }
    return 0;
}

3.3 异步选择模型(WSAAsyncSelect)

WSAAsyncSelect 模型允许应用程序以Windows消息的形式接收网络事件通知,目的是适应Windows的消息驱动环境设置的,目前对性能要求不高的网络应用程序都采用WSAAsyncSelect 模型。MFC中的CSocket也有采用

3.4 事件通知(WSAEventSelect)

也是异步事件通知I/O模型
允许应用程序在一个或多个套接字上接收基于事件的网络通知
并不依靠Windows的消息驱动机制,由事件对象的句柄通知

3.4.1 WSAEventSelect 函数

基本步骤

  1. 创建事件对象
  2. 事件对象与网络事件关联
  3. 网络事件发生,使事件对象受信,在事件对象上的等待函数就会执行,返回
  4. 调用WSAEnumNetworkEvents 函数 知道发生什么网络事件

创建事件对象

WSAEVENT WSACreateEvent(void);

WSAEventSelect

int WSAEventSelect(
	SOCKET s, // 套接字句柄
	WSAEVENT hEventObject, // 事件对象句柄
	long INetworkEvents // FD_XXX 网络事件的组合
);

WSAWaitForMultipleEvents 函数,等待

DWORD WSAWaitForMultipleEvents(
	DWORD cEvents, // lphEvents 所指的数组大小
	const WSAEVENT* lphEvents, // 事件对象句柄数组
	BOOL fWaitAll, // 是否所有事件对象都变成受信状态
	DWORD dwTimeout, // 指定等待时间,WSA_INFINITE 为无穷大
	BOOL fAlertable // 使用WSAEventSelect 可以忽略,为FALSE
);

WSAWaitForMultipleEvents 最多支持WSA_MAXIMUM_WAIT_EVENTS 个对象 被定义为64
在一个线程中同一时间最多管理64个套接字
超出64需创建额外工作线程

返回值

  • 超出时间,WSA_WAIT_TIMEOUT
  • 有事件发生,返回事件对象
  • 调用失败返回WSA_WAIT_FAILED

WSAEnumNetworkEvents

int WSAEnumNetworkEvents(
	SOCKET s, // 套接字句柄
	WSAEVENT hEventObject, // 对应事件对象句柄,调用后重置事件对象状态
	LPWSANETWORKEVENTS lpNetworkEvents // 指向一个网络事件的结构
);

WSANETWORKEVENTS

typedef struct _WSANETWORKEVENTS {
	long INetworkEvents; // 指定发生的网络事件
	int iErrorCode[FD_MAX_EVENTS]; // 与INetworkEvents 相关的出错代码
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS

3.4.2 应用举例

WSAEventSelect 编程的基本步骤如下

  1. 创建一个事件句柄表和一个对应的套接字句柄表
  2. 创建一个套接字,对应创建一个事件,放入步骤1创建的表中,并调用WSAEventSelect函数关联起来
  3. 调用WSAWaitForMultiple 在所有事件上等待,函数返回后遍历事件数组,调用WSAWaitForMultiple 函数判断每个事件是否受信
  4. 处理发生的网络事件,继续在事件上等待

3.5 重叠 OverLapped I/O 模型

该模型提供更好的性能。运行应用程序使用重叠数据结构一次投递一个或者多个的异步I/O请求

3.5.1 重叠I/0 函数

1、创建套接字

SOCKET WSASocket(int af, int type, int protocol,
	LPWSAPROTOCOL_INFO lpProtocolInfo, // 指定下层服务提供者,可以是NULL
	GROUP g, // 保留
	DWORDd dwFlags // 指定套接字属性,指定WSA_FLAG_OVERLAPPED
);

创建监听套接字

SOCKET sListen = ::WSASocket(AF_INET, SOCK_STREAM, IP_PROTOCOL, NULL, 0, WSA_FLAG_OVERLAPPPED);

2、传输数据

WSASend

int WSASend(
	SOCKET s,
	LPWSABUF lpBuffers, // WSABUF结构的数组
	DWORD dwBufferCount, // 上面数组大小
	LPDWORD lpNumberOfBytesSent, // I/O 操作立即完成,获得实际传输数据的字节数
	DWORD dwFlags. // 标志
	LPWSAOVERLAPPED lpOverlapped, // 与此I/O 操作关联的WSAOVERLAPPED结构
	LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 指定一个完成例程
);

3、接受连接
AcceptEx

BOOL AcceptEx(
	SOCKET sListen Socket, // 监听套接字句柄
	SOCKET sAcceptSocket, // 指定一个未被使用的套接字,在这个套接字上接收新的连接
	PVOID lpOutputBuffer, // 指定一个缓冲区,取得新连接的第一块数据,服务器的本地地址和客户端地址
	DWORD dwReceiveDataLength, // lpOutputBuffer 指的缓冲区大小
	DWORD dwLocalAddressLength, // 为本地地址预保留的长度
	DWORD dwRemoteAddressLength, // 为远程地址预留的长度
	LPDWORD lpdwBytesReceived, // 取得接收数据的长度
	LPOVERLAJPJPED lpOverlapped // 处理本请求的OVERLAPPED结构,不能为NULL
);

该函数声明在Mswsock.h中,需添加Mswsock.lib库

第五章 互联网广播和IP多播

在广播通信中,网络层提供了将一个节点发送到其他所有节点的服务:多播可以使用一个源节点发送封包的拷贝到其他多个网络节点的集合

5.1 套接字选项和I/O 控制命令

5.1.1 套接字选项

获取和设置套接字选项的函数是 getsockoptsetsockopt 结构用法如下

int getsockopt(
	SOCKET s,
	int level,// 定义该选项在OSI的哪个级别
	int optname, // 选项名称
	char* optval, // 指定一个缓冲区,请求选项的值会在这
	int* optlen // 上面缓冲区的大小
);

int setsockopt(SOCKET s, int level, int optname, const char* optval, int optlen);

1、SOL_SOCKET级别 应用层

  • SO_BROADCAST: BOOL类型,设置套接字传输和接收广播消息。
  • SO_REUSEADDR: BOOL类型,值为TRUE,套接字可以绑定到一个已经被另一个套接字使用的地址,或者处于TIME_WAIT 的地址
  • SO_RCVBUF和SO_SNDBUF: int类型,获取或设置套接字内部为接收(发送)操作分配缓冲区的大小‘
  • SO_RCVTIMEO和SO_SNDTIMEO: int类型,获取或设置套接字接收(发送)的超时值(ms)
    2、IPPROTO_IP级别 网络层
  • IP_HDRINCL: BOOL类型。值为TRUE,IP头和数据会一块提交给winsock发送调用,
  • IP_TTL: int类型。设置和获取IP头中的TTL参数。数据报使用TTL域来限制它能够经过的路由器数量,防止路由陷入死循环

5.1.2 I/O 控制命令

下面是一些常用的ioctl命令

  • FIONBIO: 将套接字置于非阻塞模式
  • SIO_RCVALL: 接收网络上所有的封包。抓包

嗅探器程序向套接字发送SIO_REVALL控制命令的代码

DWORD dwValue = 1;
if(ioctlsocket(sRaw, SIO_RCVALL, &dwValue) != 0)
	return;

5.2 广播通信

服务端

#include <stdio.h>
#include <winsock2.h>
#include "initsock.h"

CInitSock c;

int main()
{
    // create socket
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, 0);
    
    // set SO_BROADCAST
    BOOL bBroadCast = TRUE;
    ::setsockopt(s, SOL_SOCKET, SO_BROADCAST, (char*)&bBroadCast, sizeof(bBroadCast));
    // bind
    SOCKADDR_IN bcast;
    bcast.sin_family = AF_INET;
    bcast.sin_addr.S_un.S_addr = INADDR_BROADCAST;
    bcast.sin_port = htons(4567);
    // send
    printf(" 开始向4567端口发送广播数据... \n \n");
	char sz[] = "我的天 \n";

    while(TRUE)
    {
        ::sendto(s, sz, strlen(sz), 0, (SOCKADDR*)&bcast, sizeof(bcast));
        Sleep(5000);
    }

    return 0;
}

接收端

#include <stdio.h>
#include "initsock.h"

CInitSock c;

int main()
{
    // create socket
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, 0);
    // bind
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_addr.S_un.S_addr = INADDR_ANY;
    sin.sin_port = htons(4567);
    if(::bind(s, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        printf(" bind() failed \n");
		return 0;
    }
    
    // recv
    printf(" 开始在4567端口接收广播数据... \n\n");
    SOCKADDR_IN addrRemote;
    int nLen = sizeof(addrRemote);
    char sz[256];
    while(TRUE)
    {
        int nRet = ::recvfrom(s, sz, 256, 0, (sockaddr*)&addrRemote, &nLen);
        if(nRet > 0)
        {
            sz[nRet] = '\0';
            printf("收到以下数据: \n%s\n\n", sz);
        }
    }

    return 0;
}

5.3 IP 多播

广播发送到所有节点,多播发送到一些节点的集合去,不发送全部

5.3.1 多播地址

5.3.3 使用IP多播

1、加入组和离开组

  • 选项级别level :IPPROTO_IP
  • 选项名称optname:IP_ADD_MEMBERSHIP、IP_DROP_MEMBERSHIP
  • 请求该选项要带的数据:ip_mreq结构体

ip_mreq

typedef struct
{
	struct in_addr imr_multiaddr; // 多播组的ip地址
	struct in_addr imr_interface; // 要加入或离开的本地地址
}ip_mreq;

加入示例

iq_mreq mcast;
mcast.imr_interface.S_un.S_addr = INADDR_ANY;
mcast.imr_multiaddr.S_un.S_addr = ::inet_addr("234.5.6.7");
int nRet = ::setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mcast, sizeof(mcast));

离开示例

iq_mreq mcast;
mcast.imr_interface.S_un.S_addr = dwInterFace;
mcast.imr_multiaddr.S_un.S_addr = dwMultiAddr
int nRet = ::setsockopt(s, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char*)&mcast, sizeof(mcast));

3、发送多播数据

TTL的值的各个概念

  • 初始0——多播封包限制在同一个主机
  • 初始1——限制在同一个子网
  • 初始32——限制在同一个站点
  • 初始64——限制在同一个地区
  • 初始128——限制在同一个大陆
  • 初始255——无地区限制

第六章 原始套接字

允许访问底层传输协议

6.1 使用原始套接字

有两种类型,在IP头中预定义的,在IP头中使用自定义的

6.2 ICMP编程

6.2.1 ICMP与校验和的计算

1、ICMP格式

第一个域是消息类型,第二个域是代码,第三个域是校验和

类型 询问/错误类型 代码 描述
0 询问 0 回显应答
3 错误:目标不可达 0 网络不可达
8 询问 0 请求回显
11 错误:超时 0 传输过程中TTL等于0

2、校验和的计算

USHORT checksum(USHORT* buff, int size)
{
	ungigned long cksum=0;
	while(size > 1)
	{
		cksum += *buffer++;
		size -= sizeof(USHORT);
	}

	if(size)
	{
		cksum += *(UCHAR*)buffer;
	}
	chsum = (chsum >> 16) + (cksum & 0xffff);
	cksum += (cksum >> 16);
	return (USHORT)(~cksum);
}

6.2.2 ping 程序实例

基本编程步骤如下

  1. 创建协议类型为IPPROTO_ICMP 的原始套接字,设置其属性
  2. 创建并初始化ICMP封包
  3. 调用sendto向远程主机发送ICMP请求
  4. 调用recvfrom函数接收ICMP响应

6.2.3 路由跟踪

#include "initsock.h"
#include "protoinfo.h"
#include "comm.h"

#include <stdio.h>
#include <windows.h>

CInitSock c;

typedef struct icmp_hdr
{
    unsigned char icmp_type;
    unsigned char icmp_code;
    unsigned short icmp_checksum;
    
    unsigned short icmp_id;
    unsigned short icmp_sequence;
    unsigned long icmp_timestamp;
} ICMP_HDR, *PICMP_HDR;

int main()
{
    char * szDestIp = "192.168.90.160";
    char recvBuf[1024] = {0};

    // 创建用于接收ICMP封包的原始套节字,绑定到本地端口
    SOCKET sRaw = ::socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
	sockaddr_in in;
	in.sin_family = AF_INET;
	in.sin_port = 0;
	in.sin_addr.S_un.S_addr = INADDR_ANY;
	if(::bind(sRaw, (sockaddr*)&in, sizeof(in)) == SOCKET_ERROR)
	{
		printf("bind() failed \n");
		printf("bind() 失败,错误码:%d\n", WSAGetLastError());
		system("pause");
		return 0;
	}
    SetTimeout(sRaw, 5*1000);

    // 创建用于发送UDP封包的套节字
    SOCKET sSend = ::socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

    SOCKADDR_IN destAddr;
    destAddr.sin_family = AF_INET;
    destAddr.sin_addr.S_un.S_addr = ::inet_addr(szDestIp);
    destAddr.sin_port = ::htons(22);

    int nTTL = 1;
    int nRet;
    ICMP_HDR* pICMPHdr;
    int nTick;
    SOCKADDR_IN recvAddr;
    do
    {
        // 设置UDP封包的TTL值
        SetTTL(sSend, nTTL);
        nTick = ::GetTickCount();
        
        // 发送这个UDP封包
        nRet = ::sendto(sSend, "hello", 5, 0, (sockaddr*)&destAddr, sizeof(destAddr));
        if(nRet == SOCKET_ERROR)
        {
            printf(" sendto() failed \n");
			break;
        }

        // 等待接收路由器返回的ICMP报文
        int nLen = sizeof(recvAddr);
        nRet = ::recvfrom(sRaw, recvBuf, 1024, 0, (sockaddr*)&recvAddr, &nLen);
        if(nRet == SOCKET_ERROR)
        {
            if(::WSAGetLastError() == WSAETIMEDOUT)
			{
				printf(" time out \n");
				break;
			}
            else
			{
				printf(" recvfrom() failed \n");
				break;
			}
        }

        // 解析接收到的ICMP数据
        pICMPHdr = (ICMP_HDR*)&recvBuf[20];
        if(pICMPHdr->icmp_type != 11 && pICMPHdr->icmp_type != 3 && pICMPHdr->icmp_code != 3)
        {
            printf(" Unexpected Type: %d , code: %d \n",
				pICMPHdr->icmp_type, pICMPHdr->icmp_code);
        }
        else
        {
            char* szIP = ::inet_ntoa(recvAddr.sin_addr);
            printf(" 第%d个路由器,IP地址:%s \n", nTTL, szIP);
			printf("      用时:%d毫秒 \n", ::GetTickCount() - nTick);
        }

        if(destAddr.sin_addr.S_un.S_addr == recvAddr.sin_addr.S_un.S_addr)
		{	
			printf("目标可达 \n");
			break;
		}

        printf("//------------------------------------// \n");

    } while (nTTL++ < 20);
    
    ::closesocket(sRaw);
	::closesocket(sSend);
	system("pause");
    return 0;
}

BOOL SetTTL(SOCKET s, int nValue)
{
    int result = setsockopt(s, IPPROTO_IP, 4, (const char*)&nValue, sizeof(nValue)); // IP_TTL 在win10已经删除 
    if (result == SOCKET_ERROR)
    {
        printf("setsockopt() 失败,错误码:%d\n", WSAGetLastError());
        return FALSE;
    }
    
    return TRUE;
}

// 设置给定套接字的接收/发送超时时间(毫秒)
BOOL SetTimeout(SOCKET s, int nTime, BOOL bRecv /*= TRUE*/)
{
    DWORD timeoutValue = static_cast<DWORD>(nTime);

    int option = bRecv ? SO_RCVTIMEO : SO_SNDTIMEO;
    int result = setsockopt(s, SOL_SOCKET, option, (const char*)&timeoutValue, sizeof(timeoutValue));
    if (result == SOCKET_ERROR)
    {
        printf("setsockopt() 失败,错误码:%d\n", WSAGetLastError());
        return FALSE;
    }

    return TRUE;
}

6.3 使用IP头包含选项

6.3.1 IP 数据报格式

  • Version——这4位指定IP数据报版本,对应IPv4来说,值为4
  • IHL(IP Header Length)——IP头不固定,需要4位确定数据开始部分,一般不包含该域,通常的IP数据报有20个字节的头长度
  • Type of serviec——区分不同类型的IP数据报
  • Total Length——IP数据报的总长度,IP头+数据,理论最长是65535字节,但数据报长度很少超过1500
  • Identification——标识已发送的数据报,发送一次封包,增加一次值
  • Flags和Fragment offset——给路由器的指令,DF代表不分割数据报,MF代表更多的分割
  • Time to live——TTL,超时检测,数据报被路由器处理,值减1,为0就丢弃
  • Protocol——IP数据报到达目的使用此域,指定数据部分传递给哪个传输层协议,6位TCP,17为UDP
  • Header checksum——头校验
  • Source address 和 Destination address——源IP、目的IP
  • Options——可添加额外信息,最多40字节

6.3.2 UDP数据报格式

  • Source port和Destination port——源端口,目的端口,都16位
  • UDP length——UDP长度
  • UDP checksum——UDP校验和

UDPHEADER描述UDP头

typedef struct _UDPHeader{
	USHORT sourcePort; // 源端口号
	USHORT destinationPort;
	USHORT len;
	USHORT checksum;
};

6.4 网络嗅探器开发实例

6.4.1 嗅探器设计原理

TCP头格式

第十四章 Email协议及其编程

SMTP POP3 这两个协议 原理及其实现

14.1 概述

个人用户不能直接收发电子邮件,通过ISP主机的一个电子信箱负责接收

14.2 电子邮件介绍

电子邮件的格式由3部分组成:信头、信体、签名区

14.2.1 电子邮件的Internet地址

通用形式为:userid(用户标识)@doman(域名)
ex: lrzshaha@qq.com

14.2.3 电子邮件的信头结构及分析

1、邮件的结构
(1) From: xx@xx
(2) To: xx@xx
(3) Subject: xxx
(4) Date: Thu, 21 Dec 2023. 03:00:00 GMT+8
(5)
(6) xxx
(7) This mail is to explain you the mail format
(8) ----
(9) Thanks
(10) lrui1

RFC822 要求 信头和信体间要有一个空行,就是(5)

14.3 SMTP协议原理介绍

SMTP基本命令

14.4 POP3协议原理介绍

14.4.1 POP3协议简介

POP——Post Office Protocol 邮局协议,使用TCP110端口,POP3仍然采用C/S模式

14.4.2 POP3工作原理

POP3协议有三种工作状态,确认状态、操作状态、更新状态

14.4.3 POP3命令原始码

参考文献

《Windows网络与通信程序设计(第3版)——陈香凝..》