当前位置: 首页 > news >正文

【安全扫描器原理】TCP/IP协议编程

【安全扫描器原理】TCP/IP协议编程

  • 1.概述
  • 2.Windows Socket结构
  • 3.Windows socket转换类函数
  • 4.Windows Socket通信类函数

1.概述

TCP/IP协议是目前网络中使用最广泛的协议,Socket称为“套接口”​,最早出现在Berkeley Unix中,最初只支持TCP/IP协议族和Unix协议,现在它已支持很多协议,是最重要的网络编程接口

1、端口(Port)和套接口

端口正是我们要扫描的对象,具有“开”和“关”两种状态,利用它的开或关状态就可以初步判断一台主机是否提供了某种服务

和端口所在主机的IP地址结合起来,所形成的一个二元组(IP地址,端口地址)就组成了一个套接口。一个五元组(本地IP、本地端口、使用协议、远程IP、远程端口)组成了一个通信过程

一个IPv4的基本数据结构主要有in_addrsockaddr_in两个,前者表示32位的IP地址,后者是通用的套接口地址结构

// in_addr 是用来表示一个 IPv4 地址(32 位) 的结构体
// s_addr:一个 32 位的整数,表示 IP 地址
struct in_addr {in_addr_t s_addr;
};
struct sockaddr_in {short            sin_family;   // 地址族,必须是 AF_INET(IPv4)unsigned short   sin_port;     // 端口号(使用网络字节序)struct in_addr   sin_addr;     // IPv4 地址char             sin_zero[8];  // 填充字段,保持结构体大小一致(通常置为0)
};

2、地址表示顺序

而网络传输中,数据存储顺序不一定和系统存储顺序一样,因此为保证系统正确性和可移植性,需要利用系统的转换函数进行转换。以IPv4的地址为例,一个IP地址的四个字节“192.168.1.100”​,在PC架构的计算机中,数据的表示是低位优先​,由前至后是100、1、168、192;而在网络Socket协议所表示的网络传输中,则是高位优先​,由前至后是192、168、1、100,这需要在处理时通过函数转化

3、面向连接和面向非连接

面向连接(即TCP)的通信的双方,发起连接的为客户端,接收连接的一方称为服务器端。双方的通信一般分三步:建立连接、数据传送、释放连接。在传送过程中数据按顺序传送

面向非连接(即UDP)的通信中,没有客户端和服务器端之分,或者称为互为客户端和服务器端。双方中的任何一方都可以随时向对方发送数据或接收对方的数据

面向连接和面向非连接区别:

  • 面向连接的情况下,函数会明确告诉发送成功,但对方未接到;而面向非连接的情况下,函数只是告诉发送成功,不会告诉对方是否接到
  • 两台电脑之间连接了一个小时未通信,在面向连接的方式下,这两台电脑之间会不停地发送确认信息以确定链路是否连通;而面向非连接的方式则没有这些维护信息

4、原始套接字

如果不使用原始套接字,则无论是发送和接收,系统都会自动处理IP包头、TCP/UDP包头的数据,这时用户只需要关心发送和接收的数据本身即可。这种自动处理虽然方便,但也使系统失去了灵活性。而当使用原始套接字时,如果发送数据,系统会将要发送的数据包的前面若干字节数据IP头、TCP/UDP头;如果接收数据,系统会将接收到的数据包前面加上数据IP头、TCP/UDP头


2.Windows Socket结构

1、sockaddr结构

该结构用于保存一个IP地址,但这个结构不包含具体协议字段,因此通常不直接使用,而是作为接口参数来传递实际结构(如 sockaddr_in

struct sockaddr {unsigned short sa_family; // 地址族(如 AF_INET)char sa_data[14];         // 存放端口号和地址
};

2、sockaddr_in结构

这个结构是IPv4专用的结构,包含了 IP 地址和端口号的具体信息

struct sockaddr_in {short sin_family;           // 地址族,一般是 AF_INETunsigned short sin_port;   // 端口号(需要用 htons() 转换为网络字节序)struct in_addr sin_addr;   // IP 地址char sin_zero[8];          // 补齐用,不使用,需填0
};

其中真正存储IP结构的sin_addr变量又是一个结构,内部是个联合体,有三种访问方式:

struct in_addr {union {struct {unsigned char s_b1, s_b2, s_b3, s_b4;} S_un_b;     // 按字节访问IPstruct {unsigned short s_w1, s_w2;} S_un_w;     // 按双字节访问IPunsigned long S_addr;  // 一般使用这个字段(网络字节序)} S_un;
};

在实际使用中,可以这样访问和赋值:

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

有的服务器有多个网卡,此时会有多个IP地址,或是一个网卡配置多个IP地址,而当前的程序并不想只绑定某一个IP地址,这时可以设置S_addr为htonl(INADDR_ANY):

addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

3、hostent结构

主要用于存储主机的解析信息,比如域名解析成 IP 后的结果

struct hostent {char  *h_name;        // 官方主机名(如:www.example.com)char **h_aliases;     // 主机别名(一个以NULL结尾的字符串数组)short  h_addrtype;    // 地址类型(如 AF_INET 表示 IPv4)short  h_length;      // 地址长度(IPv4是4字节)char **h_addr_list;   // 地址列表(每个都是一个 IP 地址的指针)
};

4、servent结构

通常是用于 服务(Service)名到端口号的解析

struct servent {char  *s_name;      // 服务名称(如 "http")char **s_aliases;   // 服务别名(以 NULL 结尾的字符串数组)short  s_port;      // 端口号(网络字节序)char  *s_proto;     // 协议名称(如 "tcp" 或 "udp")
};

3.Windows socket转换类函数

1、htons函数

htons函数将计算机存储的USHORT格式转换为网络存储的USHORT格式,16位,一般同于传输端口

网络传输要求所有多字节数值统一为 大端格式(高位在前),以保证跨平台通信一致性。因此,在将端口号等数值用于网络传输前必须调用 htons()

struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(80);  // 将主机字节序的端口80转换为网络字节序
server_addr.sin_addr.s_addr = inet_addr("192.168.1.1");

2、ntohs函数

ntohs函数将网络存储的USHORT格式转换为计算机存储的USHORT格式

3、htonl函数

htonl() 主要用于 IP 地址等需要使用 32 位整数传输的场景。将计算机存储的ULONG格式转换为网络存储ULONG格式

4、ntohl函数

ntohl函数将网络存储的ULONG格式转换为计算机存储的ULONG格式,是32位的

5、inet_ntoa函数

inet_ntoa函数将由in_addr结构所表示的网络地址,转换成由字符串表示的IP地址

6、inet_addr函数

inet_addr函数将字符串组成的IP地址串转换成一个ULONG的整数,该整数可用于in_addr结构中,是按网络格式存储的

7、gethostbyname函数

gethostbyname函数根据主机名读取主机的信息(主要是IP地址)​

8、gethostbyaddr函数

gethostbyaddr函数通过网络地址读取主机信息

9、gethostname函数

gethostname函数读取本地主机的主机名

10、getservbyname函数

getservbyname函数根据服务名和协议读取服务信息

11、getservbyport函数

getservbyport函数根据端口和协议读取服务信息


4.Windows Socket通信类函数

在使用 Windows Socket 通信类函数时,需要注意以下几点:

  • 版本要求:这些函数至少基于 Winsock 1.1 版本或更高版本(如 Winsock 2.2)。在编程时通常使用的是 Winsock 2.x
  • 头文件引入:在源文件中需要包含头文件 Winsock2.h
  • 链接库设置:在链接阶段需要链接 Ws2_32.lib 静态库,否则编译时会出现未解析的外部符号错误
#include <Winsock2.h>
#pragma comment(lib, "Ws2_32.lib")

1、WSAStartup函数

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

WSAStartup 用于初始化 Windows Sockets 2(WinSock2)库,它会检查操作系统是否支持所请求的 Socket 版本,并建立底层通信机制。必须最先调用该函数,否则之后的所有 Socket API 都不能使用

程序启动↓
调用 WSAStartup()↓
判断版本支持 & 初始化成功?↓                  ↘是 → 继续使用 Socket     否 → 返回错误码(不能使用 WSAGetLastError)

代码案例:

#include <Winsock2.h>
#pragma comment(lib, "Ws2_32.lib")int main() {WSADATA wsaData;int result = WSAStartup(MAKEWORD(2, 2), &wsaData);if (result != 0) {printf("WSAStartup failed: %d\n", result);return 1;}// 后续 Socket 操作...WSACleanup(); // 使用完成后清理return 0;
}

2、WSACleanup函数

int WSACleanup(void);

WSACleanup函数完成与socket库绑定的解除,并释放socket库所占用的系统资源。该函数应该作为某次socket操作的最后一个函数,否则之后任何socket操作都会导致出错

3、socket函数

SOCKET socket(int af, int type, int protocol);

socket 函数用于创建一个套接字(Socket),这是网络通信的起点。创建后返回一个套接字描述符(也就是一个“句柄”),用于后续的绑定、监听、发送、接收等操作

参数说明:

在这里插入图片描述

代码案例:

SOCKET s = socket(AF_INET, SOCK_STREAM, 0); // 创建一个TCP套接字
if (s == INVALID_SOCKET) {printf("Socket creation failed: %d\n", WSAGetLastError());
}

4、closesocket函数

closesocket关闭之前打开的socket套接字。在进行关闭之前,一般要通过shutdown函数通知对方自己要关闭套接字

5、setsockopt函数

setsockopt 是一个用于设置 socket 参数选项的函数,简单来说,它就是用来“调节” socket 行为的,比如:是否允许端口复用、是否开启 TCP 保活、缓冲区多大等

int setsockopt(SOCKET s,           // 要设置的 socket 描述符int level,          // 设置的协议级别(SOL_SOCKET/IPPROTO_TCP 等)int optname,        // 选项名(SO_REUSEADDR 等)const char *optval, // 选项值的指针int optlen          // 选项值的长度
);

调用时机图:

程序启动↓
创建套接字(socket)↓┌────────────────────────────────────┐│  是否需要更改socket默认参数?       │└────────────────────────────────────┘↓ 是                            ↓ 否调用 setsockopt 设置参数              继续↓┌────────────────────────────────────┐│  设置参数是否与 bind 阶段相关?     │└────────────────────────────────────┘↓ 是(如SO_REUSEADDR)↓注意调用时机:必须在 bind 之前设置否则 bind 可能失败↓
调用 bind 绑定地址和端口↓
后续 listen / connect 操作

代码案例:使用 setsockopt 设置端口复用(SO_REUSEADDR)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>  // Windows 下 socket 头文件
#pragma comment(lib, "ws2_32.lib") // 链接 ws2_32 库int main() {WSADATA wsaData;SOCKET listenSocket;struct sockaddr_in serverAddr;int opt = 1; // 要设置的选项值int ret;// 初始化 Winsock 库if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {printf("WSAStartup failed: %d\n", WSAGetLastError());return 1;}// 创建 socketlistenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (listenSocket == INVALID_SOCKET) {printf("socket failed: %d\n", WSAGetLastError());WSACleanup();return 1;}// 设置 socket 选项:允许地址复用(端口复用)ret = setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));if (ret == SOCKET_ERROR) {printf("setsockopt failed: %d\n", WSAGetLastError());closesocket(listenSocket);WSACleanup();return 1;}// 准备绑定地址serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本地任意 IPserverAddr.sin_port = htons(8888); // 绑定端口 8888// 绑定 socketret = bind(listenSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));if (ret == SOCKET_ERROR) {printf("bind failed: %d\n", WSAGetLastError());closesocket(listenSocket);WSACleanup();return 1;}// 开始监听连接ret = listen(listenSocket, SOMAXCONN);if (ret == SOCKET_ERROR) {printf("listen failed: %d\n", WSAGetLastError());closesocket(listenSocket);WSACleanup();return 1;}printf("Server is listening on port 8888...\n");// 清理资源(这里只是简单示例,实际应加入 accept 处理逻辑)closesocket(listenSocket);WSACleanup();return 0;
}

6、select函数

select 用来检测一组 socket 是否就绪(可读、可写或异常),从而避免阻塞或轮询所有 socket,提高程序效率

比如,你写了个服务器,要同时监听100个客户端连接,但你又不想一个个轮询问:“有数据吗?”,这个时候就用 select 让系统帮你监视,哪个 socket 有事发生(有数据可读),就去处理它

int select(int nfds,               // Linux 中使用;Windows 忽略fd_set *readfds,        // 可读 socket 集合fd_set *writefds,       // 可写 socket 集合fd_set *exceptfds,      // 异常 socket 集合const struct timeval *timeout // 超时等待时间
);

参数解释:

在这里插入图片描述

代码案例,使用 select 等待 socket 可读:

#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")int main() {WSADATA wsaData;SOCKET sock;struct sockaddr_in server;fd_set readfds;struct timeval timeout;int ret;WSAStartup(MAKEWORD(2, 2), &wsaData);sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);server.sin_family = AF_INET;server.sin_port = htons(80);server.sin_addr.s_addr = inet_addr("93.184.216.34"); // example.comconnect(sock, (struct sockaddr*)&server, sizeof(server));// 初始化 fd_setFD_ZERO(&readfds);FD_SET(sock, &readfds);// 设置超时时间为5秒timeout.tv_sec = 5;timeout.tv_usec = 0;ret = select(0, &readfds, NULL, NULL, &timeout);if (ret == 0) {printf("Timeout, no data received.\n");} else if (ret < 0) {printf("select error: %d\n", WSAGetLastError());} else {if (FD_ISSET(sock, &readfds)) {printf("Data is available to read.\n");}}closesocket(sock);WSACleanup();return 0;
}

7、bind函数

bind 函数的作用是将 socket 与本地的 IP 地址和端口号绑定起来,这样系统才知道这个 socket 是用哪个本地地址和端口通信的。该函数既可用于面向连接的TCP通信中,也可以用于面向非连接的UDP通信中

int bind(SOCKET s,                      // 已创建的 socket 描述符const struct sockaddr *name,  // 绑定的地址信息int namelen                   // 地址结构长度
);

案例:

#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib") // 链接 Winsock 库int main() {WSADATA wsaData;SOCKET serverSocket;struct sockaddr_in serverAddr;// 初始化 WinsockWSAStartup(MAKEWORD(2, 2), &wsaData);// 创建 TCP socketserverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (serverSocket == INVALID_SOCKET) {printf("Socket creation failed: %d\n", WSAGetLastError());return 1;}// 设置地址信息serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地 IPserverAddr.sin_port = htons(8080);       // 监听端口 8080// 绑定 socket 和地址if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {printf("Bind failed: %d\n", WSAGetLastError());closesocket(serverSocket);WSACleanup();return 1;}printf("Bind successful on port 8080.\n");// 开始监听连接listen(serverSocket, SOMAXCONN);printf("Listening...\n");WSACleanup();return 0;
}

8、listen函数

listen函数使用socket状态监听状态,并等待其他socket的连接。该函数仅用于面向连接的TCP通信中,UDP通信是不需要listen函数的

int listen(SOCKET s, int backlog);

参数解释:

在这里插入图片描述

9、accept函数

accept函数允许和接收一个远端的连接,该函数仅用于面向连接的TCP通信中,并且只用于服务器端,用于接收客户端通过connect函数发来的连接申请;面向非连接的UDP通信是不需要处理此函数的

可以把 listen 比喻成“门卫准备好了”,而 accept 就是“打开门迎客”那一刻

SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);

参数解释:

在这里插入图片描述

该函数看似简单,其实比较复杂,也是多线程处理效果的关键,首先调用此函数之前,应该已成功地调用了listen函数。然后在调用该函数时,如果调用成功,则返回一个新的socket,所以如果后面服务端的处理很简单,可以在当前线程中用这个新创建的socket进行处理,俗称“短连接”​;

如果处理很复杂,并且仍在当前线程中处理,则会影响到accept函数对其他线程通过connect进行连接,此时就需要再创建一个线程,由新建的线程,并使用返回的一个socket专门处理此次连接后的各项操作,俗称“长连接”​

代码案例:TCP服务端

#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")int main() {WSADATA wsaData;SOCKET listenSocket, clientSocket;struct sockaddr_in serverAddr, clientAddr;int clientAddrLen = sizeof(clientAddr);WSAStartup(MAKEWORD(2, 2), &wsaData);// 创建 TCP socketlistenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);// 绑定 IP + 端口serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = INADDR_ANY;serverAddr.sin_port = htons(8080);bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));// 启动监听listen(listenSocket, SOMAXCONN);printf("Server is listening on port 8080...\n");// 接受客户端连接(阻塞)clientSocket = accept(listenSocket, (SOCKADDR*)&clientAddr, &clientAddrLen);if (clientSocket == INVALID_SOCKET) {printf("Accept failed: %d\n", WSAGetLastError());closesocket(listenSocket);WSACleanup();return 1;}printf("Accepted a connection from %s:%d\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));// 发送欢迎消息const char* welcomeMsg = "Hello, client!\n";send(clientSocket, welcomeMsg, strlen(welcomeMsg), 0);// 关闭连接closesocket(clientSocket);closesocket(listenSocket);WSACleanup();return 0;
}

10、connect函数

在扫描器的应用中,connect是一种简单而有效的连接方式,连接成功,则可以认为对方的端口是打开的。该函数仅用于面向连接的TCP通信中,并且只用于服务器端,用来接收客户端通过connect函数发来的连接申请;面向非连接的UDP通过是不需要处理此函数的

11、send函数

send函数发送数据到已建立连接的socket上,该函数既可以用于服务器端,也可以用于客户端,但双方都必须是采用TCP连接

12、recv函数

recv函数用于接收从已建立连接的socket上的数据,该函数既可以用于服务器端,也可以用于客户端,但双方都必须是采用TCP连接。

13、shutdown函数

shutdown 函数用于关闭某个方向上的数据传输通道:只收、不收、只发、不发,或者两个方向都关。更优雅、明确地通知对方“我不想再发/收数据了”,避免连接状态混乱

int shutdown(SOCKET s, int how);

参数说明:

在这里插入图片描述

how 参数的取值:

在这里插入图片描述

如果直接调用 closesocket(s) 而不 shutdown

  • 对方可能还在写入你的 socket,但你这边已经关掉了,数据就丢了;
  • 没有给 TCP 协议一个“四次挥手” 的机会,可能造成资源悬挂(TIME_WAIT 状态不一致);

正确关闭流程应该是:

shutdown(socket, SD_SEND);  // 发送结束,通知对方
recv(...);                  // 等对方把剩下要说的说完
closesocket(socket);        // 最后关闭 socket

14、sendto函数

sendto函数发送数据报到远端的主机指定的端口上。该函数只能用于面向非连接的通信中

15、recvfrom函数

recvfrom函数接收远端发过来的数据报。该函数只能用于面向非连接的通信中

相关文章:

  • 力扣面试150题--环形链表和两数相加
  • 【滑动窗口+哈希表/数组记录】Leetcode 438. 找到字符串中所有字母异位词
  • C语言中操作字节的某一位
  • Pandas 数据处理:长格式到宽格式的全面指南
  • 潇洒郎:ssh 连接Windows WSL2 Linux子系统 ipv6地址转发到ipv4地址上
  • SDC命令详解:使用get_cells命令进行查询
  • 数据结构------C语言经典题目(7)
  • 【沉浸式求职学习day25】【部分网络编程知识分享】【基础概念以及简单代码】
  • C语言实现贪心算法
  • PostgreSQL技术内幕29:事件触发器tag原理解析
  • 开发者专用部署工具PasteSpider的V5正式版发布啦!(202504月版),更新说明一览
  • 厚铜PCB打样全流程解析:从文件审核到可靠性测试的关键步骤
  • 华为L410上制作内网镜像模板:在客户端配置模板内容
  • 1.10软考系统架构设计师:优秀架构设计师 - 练习题附答案及超详细解析
  • Jetpack Compose 基础组件学习2.1:Surface
  • VuePress可以做什么?
  • Centos 7 ssh连接速度慢(耗时秒+)
  • 视频噪点多,如何去除画面噪点?
  • Kafka 架构设计和组件介绍
  • Golang | 位运算
  • QFII一季度现身超300家公司:持有南京银行市值最高,5家青睐立航科技
  • 洛阳原副市长收礼品消费卡,河南通报6起违反八项规定典型问题
  • 俄罗斯称已收复库尔斯克州,普京发表讲话
  • 中国太保一季度净赚96.27亿元降18.1%,营收同比下降1.8%
  • 【社论】以“法治之盾”护航每一份创新
  • 特朗普签署行政命令推动深海采矿,被指无视国际规则,引发环境担忧