网络编程—使用C语言实现发送TCP数据包,以命令行形式运行:SendTCP source_ip source_port dest_ip dest_port;(原理和常见错误分析)

任务要求:

1.以命令行形式运行:SendTCP source_ip source_port dest_ip dest_port;
2.头部参数自行设定,数据字段为“This is my homework of network of network,I am happy!”;
3.成功发送后在屏幕上输出“send OK”。

需求分析

本系统要求使用C语言作为基本开发语言,并且开发工具为绿色软件,程序运行不需要安装和避免写系统和注册表。需要完成的需求如下:

  • 1.本程序需完成发送一个TCP数据包给目的主机,应用层将需要传送的信息传送给
    TCP层,TCP传输实体根据程序运行时输入的源IP地址、源端口号、目的IP地址、目的端口号加上TCP报头,形成TCP数据包,在TCP数据包上增加IP头部,形成IP包。
  • 2.运行程序时输入:SendTCP source_ip source_port dest_ip dest_port,然后根据提示输入要发送的数据,回车即可。
  • 3.程序的输出:Send OK!
  • 4.测试数据 SendTCP 172.27.91.240 200 172.27.91.123 100
    ,其中源IP地址、源端口号、目的IP地址、目的端口号可以任意指定。

系统设计

系统总体架构

本系统的目标是发送一个TCP数据包,可以利用原始套接字来完成这个工作。整个系统由初始化原始套接字、准备与发送TCP数据包、关闭释放套接字这三个功能模块组成,在准备与发送TCP数据包功能模块里又分构造IP首部、TCP首部、TCP伪首部、填充数据包和发送数据包三个部分,由它们三个部分依次执行完成准备和发送模块功能。
三个功能模块彼此依赖,必须都实现其功能并按一定次序运行,系统才能成功实现需求,系统总体架构图如下图所示:
在这里插入图片描述

功能模块

各功能模块的关系和实现顺序如下图所示:
在这里插入图片描述

初始化原始套接字模块

首先windows 初始化socket网络库,指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节,应用程序必须在初始化socket网络库之后才能调用进一步的Windows Sockets API函数。接着创建一个能够进行网络通信的套接字, 接口的类型为原始套接字,此套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,源主机所在网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给目的主机的Socket中,使这段信息能传送到其他程序中。以上为此模块功能。

准备和发送模块

首先为构造IP首部、TCP伪首部、TCP首部部分,在此部分要设计IP首部、TCP伪首部、TCP首部的结构体类型,分别创建对应的结构体类型的对象,并根据实际情况为其各自成员变量赋值,完成IP首部、TCP伪首部、TCP首部的填充。
接着是填充数据包部分,在此部分要将包含TCP传送数据和首部的IP报文按IP首部、TCP首部、TCP数据的顺序填充到套接字的发送缓存区,为数据包的发送做好准备。
最后是通过指定函数将数据包发送出去,成功发送即实现了准备与发送模块的功能。

关闭和释放原始套接字模块

关闭套接口,释放套接描述字,并解除与Socket库的绑定并且释放socket库所占的系统资源,以上为此模块功能。
以上即为三个模块实现功能的解释。

系统实现

主要数据结构

主要有IP首部、TCP首部以及TCP伪首部的数据结构,结构体创建的依据是各报文段首部格式,将各个字段作为成员变量,按照每个字段所占位数指定该成员变量的数据类型。
宏定义将usigned char、usigned short、usigned long类型命名为UCHAR、USHORT、ULONG,UCHAR、USHORT、ULONG在编写本系统的机器中各占1字节、2字节、4字节,即8位、16位、32位。如有字段占8位,则将其字段对应成员变量设置为UCHAR类型,不足8位或超出8位但不是8的倍数的字段则几个字段合并,使其大于8位并能被8整除,以便指定数据类型。
IP首部:固定长度(20字节)+可选字段;
TCP伪首部:固定长度12字节;
TCP首部:固定长度(20字节)+选项;

初始化原始套接字模块实现

socket 即是套接字, 是网络通信的基本操作单元, 可以看做是不同主机之间进程进行双向通信的端点, 即通信双方的一种约定, 可用socket的相关函数来完成通信过程,网络通信双方之间的纽带, 应用程序在网络上发送, 接受的信息都通过socket实现,socket是操作系统的资源。本系统使用raw socket (原始套接字) ,提供对网络下层通信协议(如IP协议)的直接访问。
Windows socket 网络编程总体思路
1、初始化 Windows socket
2、创建socket
3、将socket与地址结构绑定
4、发送/接收数据
5、释放socket
6、终止Windows socket

准备与发送模块实现

构造数据结构中需要由用户设定的值有源IP地址、源端口号、目的IP地址和目的端口号,由主函数中argv[]数组将其传递到内存,再使用进行赋值。使用if语句判断是否输入符合要求,符合要求才能开始此模块。
(1) 构造TCP 伪首部:
TCP前需要加上一个伪首部,这个首部只用来计算校验和,并不真正地发送给另外一端。注意16位TCP报文长度字段,值为TCP首部和TCP数据总字节数相加,并使用htons()函数将主机的无符号短整形数转换成网络字节顺序,简单地说,使用htons()函数将该字段值的高低位互换。要大于8位才需要转换,小于8位不需理会。
(2)构造TCP首部并计算检验和:
根据实际情况为其赋值,注意16位的目的端口号字段,使用atoi()函数把输入字符串转换成整型数,函数会扫描参数字符串,跳过前面的空白字符(例如空格,tab缩进)等,直到遇上数字或正负符号才开始做转换,而在遇到非数字或字符串结束符(’\0’)才结束转换,并将结果返回。再使用htons()函数将主机的无符号短整形数转换成网络字节顺序。把TCP首部的校验和字段设置为0,将TCP伪首部和TCP报文通过memset()函数和memcpy()函数填充至检验和缓冲区,memset()函数将检验和缓冲区初始化、memcpy()函数将TCP伪首部和TCP报文拷贝至检验和缓冲区。最后通过自定义的checksum()函数完成计算检验和,并将其重新赋值给检验和字段。
checksum()自定义函数实现原理:使用二进制反码和算法,缓冲区buffer[]类型是usigned short,每两个字节一个数组项,刚好是16位,通过sizeof()算出发送数据的字节数,作为累加的判断条件,如果没有整除(即size还有余下的不足16位的部分),则加上余下的部分,此时的cksum就是相加后的结果,这个结果往往超出了16位,因为校验和是16位的,所以要将高16位和计算得到的cksum再相加,第一步相加时很可能会产生进位,因此要再次把进位移到低16位进行相加。最后结果取反即为校验和。
(3)构造IP首部: 
同前面TCP首部构造方法一样,故不再做过多累述,只是源、目的IP地址字段需要使用inet_addr() 函数将点分十进制的IPv4地址转换成网络字节序列的长整型,使用将高位和地位交换。IP首部的检验和只是依靠IP首部计算出来的,通过自定义的checksum()函数完成计算检验和,并将其重新赋值给检验和字段。
(4)填充数据包:
填充数据包就是将包含TCP数据包的IP包填充至发送缓冲区,是通过memset()函数和memcpy()函数实现,memset()函数将发送缓存区全部初始化、memcpy()函数将包含TCP数据包的IP包按照规定顺序拷贝至检验和缓冲区。
(5)发送数据包:
填充好数据包后初始化发送数据报的目的地址,根据在cmd运行时输入的目的IP地址和目的端口号构造sockaddr_in结构体dest,表示目的地址,sockaddr_in结构体如下所示:

struct sockaddr_in {
	short	sin_family;
	u_short	sin_port;
	struct in_addr	sin_addr;
	char	sin_zero[8];
};

其中sin_addr存储IP地址,使用in_addr这个数据结构。
完成一切准备工作后使用sendto()函数发送数据包,sendto() 用来将数据由指定的socket 传给对方主机. 参数s 为已创建的socket套接字,参数msg 指向发送的数据内容, 参数flags一般设0,参数to 用来指定欲传送的网络地址,即dest。 参数tolen 为sockaddr_in结构体dest长度。如果发送成功即输出sendOK!不需要管接收方是否收到报文。

关闭模块实现

关闭模块非常简单,只有一个步骤,简单但必须实现,实现通过两个函数实现,closesocket()函数关闭套接口,释放套接描述字sock,以后对sock的访问均以WSAENOTSOCK错误返回,WSACleanup()解除与Socket库的绑定并且释放socket库所占的系统资源。

系统测试(常见错误)

系统测试发生了较多问题,具体内容如下所示:
1、DEV-C++编译时报错“see previous definition of ‘AF_IPX’”、“warning C4005: ‘AF_IPX’ : macro redefinition”等等。
报错原因:头文件Windows.h在winsock.h之前导入,Windows.h中包含了winsock.h,如果先导入windows.h,那么会导入和winsock2.h冲突的winsock.h,出现报错,头文件包含了2次读取,所以头文件里面的宏会重复定义。
解决方法:头文件顺序变换,保证winsock.h在Windows.h之前,如下:
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <time.h>
#include <windows.h>
#include <string.h>
#include <stdlib.h>
#pragma comment(lib,“ws2_32.lib”)
2、 CMD中使用gcc编译程序时报错“gcc不是内部或外部命令,也不是可运行的程序或批处理文件”,具体如下图所示:
在这里插入图片描述
报错原因:没有gcc编译器,gcc编译器是Linux平台下的,Windows平台本身不支持。
解决办法:可以在DEV-C++下编译成功形成exe文件直接在cmd中运行,或者下载安装MinGW,MinGW 提供了一套简单方便的Windows下的基于GCC 程序开发环境,安装后配置好环境变量即可成功编译目标程序。
3、在编译时出现报错,具体如下图所示:
在这里插入图片描述
报错原因:编译时没有指定链接到socket库
解决办法:编译的时候在编译语句后面加上-lws2_32,指定链接到socket库即可。
4、系统运行时Socket创建失败,具体情况如下图所示:
在这里插入图片描述
错误原因:使用WSAGetLastError()函数捕获错误原因,返回值为10013,表示以一种访问权限不允许的方式做了一个访问套接字的尝试。经过网上查找资料后发现由于设置了IP_HDRINCL选项,所以必须拥有administrator权限去运行系统。
解决办法:以管理员方式运行cmd,再运行程序即可。
经过多次调试,成功在界面输出“send ok!”,成功运行结果如下图所示:
在这里插入图片描述

为了验证是否成功发送,使用WireShark进行捕获,成功捕获到数据包,需求全部成功实现,捕获结果、携带数据如下图所示:
在这里插入图片描述
在这里插入图片描述

结论评价

本系统成功地完成对TCP数据包的填充和发送。对TCP 数据包进行填充要求我们充分了解它的数据结构以及相应字节上应该存放的内容和它们的功能。在数据正确性与合法性上,TCP用校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和。在保证可靠性上,采用超时重传机制。在实现TCP数据包的发送中用到rawSocket来进行自定义IP报文的源地址,使用sendto()函数发送数据包。
windows从winxp sp2开始便对raw socket进行了限制,所以raw socket只有在Linux平台上才能发挥它本该有的全部功能。如果有条件和时间最好在Linux上实现。并且此次系统设计只考虑到了服务端的发送,并没有考虑接收端接受的问题,只是完成了基本需求,还可以更进一步,使用C/S模式完成,使服务器端和客户端能够进行交互,使系统更加完整,更能体现出TCP可靠传输的特性和实现方式,也能更加清楚了解到Socket网络编程的全部步骤,对其中一些功能函数也能够更加熟悉。
总体而言,本系统成功实现基本需求,但是还可以完善进步,改进空间还很大。大家可以考虑按上面所做的完善这个系统,工作量可能还是有点大,基本是要重新做,但是有了这个已经做好的系统的基础上手开发会快非常多。

源代码

#include <stdio.h> 
#include <winsock2.h>    //Windows的TCP/IP网络编程接口(API),就相当于连接系统和你使用的软件之间交流的一个接口     
#include <ws2tcpip.h> 
#include <time.h>
#include <windows.h>     //Windows.h中包含了winsock.h,如果先导入windows.h,那么会导入和winsock2.h冲突的winsock.h,出现报错,头文件包含了2次读取,所以头文件里面的宏会重复定义 
#include <string.h>
#include <stdlib.h>   
#pragma comment(lib,"ws2_32.lib")

#define IPVER   4           //IP协议预定
#define MAX_BUFF_LEN 65500  //发送缓冲区最大值

typedef struct ip_hdr    //定义IP首部 
{
	UCHAR h_verlen;            //4位首部长度,4位IP版本号 
	UCHAR tos;                //8位服务类型TOS 
	USHORT total_len;        //16位总长度(字节) 
	USHORT ident;            //16位标识 
	USHORT frag_and_flags;    //3位标志位和13位偏移大小 
	UCHAR ttl;                //8位生存时间 TTL 
	UCHAR proto;            //8位协议 (TCP, UDP 或其他) 
	USHORT checksum;        //16位IP首部校验和 
	ULONG sourceIP;            //32位源IP地址 
	ULONG destIP;            //32位目的IP地址 
}IP_HEADER; 

typedef struct tsd_hdr //定义TCP伪首部 
{ 
	ULONG saddr;    //源地址
	ULONG daddr;    //目的地址 
	UCHAR mbz;        //
	UCHAR ptcl;        //协议类型 
	USHORT tcpl;    //TCP长度 
}PSD_HEADER; 

typedef struct tcp_hdr //定义TCP首部 
{ 
	USHORT th_sport;            //16位源端口 
	USHORT th_dport;            //16位目的端口 
	ULONG th_seq;                //32位序列号 
	ULONG th_ack;                //32位确认号 
	UCHAR th_lenres;            //4位首部长度/6位保留字 
	UCHAR th_flag;                //6位标志位 
	USHORT th_win;                //16位窗口大小 
	USHORT th_sum;                //16位校验和 
	USHORT th_urp;                //16位紧急数据偏移量 
}TCP_HEADER; 




//CheckSum:计算校验和的子函数 
USHORT checksum(USHORT *buffer, int size) 
{ 
    unsigned long cksum=0; 
    while(size >1) 
    { 
        cksum+=*buffer++; 
        size -=sizeof(USHORT); 
    } 
    if(size) 
    { 
        cksum += *(UCHAR*)buffer; 
    } 

    cksum = (cksum >> 16) + (cksum & 0xffff); 
    cksum += (cksum >>16); 
    return (USHORT)(~cksum); 
} 

int main(int argc, char* argv[]) 
{ 
    WSADATA WSAData;   //一类数据结构,存放windows socket初始化信息 
    SOCKET sock;      
	//socket()函数创建一个能够进行网络通信的套接字,对于TCP/IP协议族,该参数置AF_INET,第二个参数指定要创建的套接字类型,第三个参数指定应用程序所使用的通信协议 
	//该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。 
    //一般编程中并不直接针对sockaddr数据结构操作,而是使用另一个与sockaddr等价的数据结构,sockaddr_in,其结构体如下: 
	    //sin_family指代协议族,在socket编程中只能是AF_INET
        //sin_port存储端口号(使用网络字节顺序)
        //sin_addr存储IP地址,使用in_addr这个数据结构
        //sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
        
    IP_HEADER ipHeader; 
    TCP_HEADER tcpHeader; 
    PSD_HEADER psdHeader; 

    char Sendto_Buff[MAX_BUFF_LEN];  //发送缓冲区
    unsigned short check_Buff[MAX_BUFF_LEN]; //检验和缓冲区
    const char tcp_send_data[]={"This is my homework of networt,I am happy!"};

    BOOL flag; 
    int rect,nTimeOver; 
    
//    BOOL Flag=TRUE; 
//    setsockopt(sock, IPPROTO_IP, IP_HDRINCL, (char *)&Flag, sizeof(Flag));
//    int timeout=1000;
//    setsockopt(sock, SOL_SOCKET,SO_SNDTIMEO,(char*)&timeout, sizeof(timeout));

    if (argc!= 5) 
    {
        printf("Useage: sendtcp soruce_ip source_port dest_ip dest_port \n"); 
        return false; 
    } 

    if (WSAStartup(MAKEWORD(2,2), &WSAData)!=0) 
	//第一个参数Windows Sockets API提供的调用方可使用的最高版本号
	//第二个参数指向WSADATA数据结构的指针,用来接收Windows Sockets实现的细节.
	//函数允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节,应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数. 
    { 
        printf("WSAStartup Error!\n"); 
        return false; 
    } 
	if((sock=WSASocket(AF_INET,SOCK_RAW,IPPROTO_RAW,NULL,0,WSA_FLAG_OVERLAPPED))==INVALID_SOCKET) 
	//socket必须是raw socket才能发送自定义的数据包
	//WSASocket()的发送操作和接收操作都可以被重叠使用。接收函数可以被多次调用,发出接收缓冲区,准备接收到来的数据。发
	//送函数也可以被多次调用,组成一个发送缓冲区队列。可是socket()却只能发过之后等待回消息才可做下一步操作
	//创建一个与指定传送服务提供者捆绑的套接口,可选地创建和/或加入一个套接口组。其功能都是创建一个原始套接字 
	//af:[in]一个地址族规范。目前仅支持AF_INET格式,亦即ARPA Internet地址格式。
	//type:新套接口的类型描述。
    //protocol:套接口使用的特定协议,这里IPPROTO_RAW,表示这个socket只能用来发送IP包,而不能接收任何的数据。发送的数据需要自己填充IP包头,并且自己计算校验和。
	//IPPROTO_IP为用于接收任何的IP数据包。其中的校验和和协议分析由程序自己完成。 
    //lpProtocolInfo:一个指向PROTOCOL_INFO结构的指针,该结构定义所创建套接口的特性。如果本参数非零,则前三个参数(af, type, protocol)被忽略,可以为空。
    //g:保留给未来使用的套接字组。套接口组的标识符。
	//iFlags:套接口属性描述 ,WSA_FLAG_OVERLAPPED表明可以使用发送接收超时设置。 
	//该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET。 
    { 
        printf("Socket Setup Error!\n"); 
        printf("send error:%d\n", WSAGetLastError());
        return false; 
    } 
    flag=true; 
	//设置选项值  IP_HDRINCL为要设置的选项值
    if(setsockopt(sock,IPPROTO_IP,IP_HDRINCL,(char*)&flag,sizeof(flag))==SOCKET_ERROR) 
    // 获取或者设置与某个套接字关联的选项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。
	//当操作套接字选项选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该将层的值指定为SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。 
	    //sock:将要被设置或者获取选项的套接字。
        //level:选项所在的协议层。
        //optname:需要访问的选项名。
        //optval:指向包含新选项值的缓冲。
        //optlen:现选项的长度 
    //这里表示为操作一个由IP协议解析的代码,所以层应该设定为IP层。即 IPPROTO_IP,设置的控制的方式(选项值)为IP_HDRINCL,意思为在数据包中包含IP首部,表明自己来构造IP头。
	//注意,如果设置IP_HDRINCL选项,那么必须拥有administrator权限。 
	{ 
        printf("setsockopt IP_HDRINCL error!\n"); 
        return false; 
    } 
    nTimeOver=1000; 
	//设置选项值  SO_SNDTIMEO为要设置的选项值
    if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char*)&nTimeOver, sizeof(nTimeOver))==SOCKET_ERROR) 
    { 
        printf("setsockopt SO_SNDTIMEO error!\n"); 
        return false; 
    } 
    //在send()过程中有时由于网络状况等原因,发送不能预期进行,而设置收发时限,在这里我们使用基本套接字SOL_SOCKET,设置SO_SNDTIMEO表示使用发送超时设置,超时时间设置为1000ms 
    //经过一次性设置后,以后调用send系列的函数,最多只能阻塞 1 秒 
    
    //填充IP首部 
    ipHeader.h_verlen=(IPVER<<4 | sizeof(ipHeader)/sizeof(unsigned long));  
    ipHeader.tos=(UCHAR)0; 
    ipHeader.total_len=htons((unsigned short)sizeof(ipHeader)+sizeof(tcpHeader)+sizeof(tcp_send_data)); 
    ipHeader.ident=0;       //16位标识,用来标识是否分片。 
    ipHeader.frag_and_flags=0; //3位标志位,最低位为MF,0则表示后面没有分片了,DF表示不能分片,分片时置为1。 
    ipHeader.ttl=128; //8位生存时间  
    ipHeader.proto=IPPROTO_UDP; //协议类型,协议类型是什么就交给哪个协议进行处理 
    ipHeader.checksum=0; //检验和暂时为0
    ipHeader.sourceIP=inet_addr(argv[1]);  //32位源IP地址
    ipHeader.destIP=inet_addr(argv[3]);    //32位目的IP地址
    //inet_addr() 函数的作用是将点分十进制的IPv4地址转换成网络字节序列的长整型,将高位和地位交换 
	//网络字节序定义:收到的第一个字节被当作高位看待,这就要求发送端发送的第一个字节应当是高位。a.b.c.d先发送a,再b、c、d。 
    
	//计算IP头部检验和
    memset(check_Buff,0,MAX_BUFF_LEN);
    //将一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化,此处是将check_buff全部初始化为‘0’ 
    memcpy(check_Buff,&ipHeader,sizeof(IP_HEADER));
    //用做内存拷贝,可以拿它拷贝任何数据类型的对象,可以指定拷贝的数据长度,strcpy只能拷贝字符串,遇到'/0'就结束拷贝
    ipHeader.checksum=checksum(check_Buff,sizeof(IP_HEADER));
    

    //构造TCP伪首部
    psdHeader.saddr=ipHeader.sourceIP;
    psdHeader.daddr=ipHeader.destIP;
    psdHeader.mbz=0;
    psdHeader.ptcl=ipHeader.proto;
    psdHeader.tcpl=htons(sizeof(TCP_HEADER)+sizeof(tcp_send_data));  //将主机的无符号短整形数转换成网络字节顺序,简单地说,htons()就是将一个数的高低位互换

    //填充TCP首部 
    tcpHeader.th_dport=htons(atoi(argv[4])); //16位目的端口号,atoi()函数用于把字符串转换成整型数,函数会扫描参数字符串,跳过前面的空白字符(例如空格,tab缩进)等,直到遇上数字或正负符号才开始做转换,而在遇到非数字或字符串结束符('\0')才结束转换,并将结果返回 
    tcpHeader.th_sport=htons(atoi(argv[2])); //16位源端口号 
    tcpHeader.th_seq=0;                         //SYN序列号
    tcpHeader.th_ack=0;                         //ACK序列号置为0
    //TCP首部的长度和保留位
    tcpHeader.th_lenres=(sizeof(tcpHeader)/sizeof(unsigned long)<<4|0); 
    tcpHeader.th_flag=2; //修改这里来实现不同的标志位探测,2是SYN,1是//FIN,16是ACK探测 等等 
    tcpHeader.th_win=htons((unsigned short)8192);     //窗口大小
    tcpHeader.th_urp=0;                            //紧急指针,指出本报文段中的紧急数据的字节数    
    tcpHeader.th_sum=0;                            //检验和暂时填为0
    
    //计算TCP校验和 
    memset(check_Buff,0,MAX_BUFF_LEN);
    memcpy(check_Buff,&psdHeader,sizeof(psdHeader)); 
    memcpy(check_Buff+sizeof(psdHeader),&tcpHeader,sizeof(tcpHeader));    //直接从 check_Buff+sizeof(psdHeader)出开始,数组名的值是个指针常量,也就是数组第一个元素的地址。 
    memcpy(check_Buff+sizeof(PSD_HEADER)+sizeof(TCP_HEADER),tcp_send_data,sizeof(tcp_send_data));
    tcpHeader.th_sum=checksum(check_Buff,sizeof(PSD_HEADER)+sizeof(TCP_HEADER)+sizeof(tcp_send_data)); 

    //填充发送缓冲区
    memset(Sendto_Buff,0,MAX_BUFF_LEN);
    memcpy(Sendto_Buff,&ipHeader,sizeof(IP_HEADER));
    memcpy(Sendto_Buff+sizeof(IP_HEADER),&tcpHeader,sizeof(TCP_HEADER)); 
    memcpy(Sendto_Buff+sizeof(IP_HEADER)+sizeof(TCP_HEADER),tcp_send_data,sizeof(tcp_send_data));
    int datasize=sizeof(IP_HEADER)+sizeof(TCP_HEADER)+sizeof(tcp_send_data);
    
    //发送数据报的目的地址
    SOCKADDR_IN dest;    
    memset(&dest,0,sizeof(dest));
    dest.sin_family=AF_INET; 
    dest.sin_addr.s_addr=inet_addr(argv[3]); //inet_addr() 函数的作用是将点分十进制的IPv4地址转换成网络字节序列的长整型,将高位和地位交换 
    dest.sin_port=htons(atoi(argv[4]));
	rect=sendto(sock,Sendto_Buff,datasize, 0,(struct sockaddr*)&dest, sizeof(dest)); 
	if (rect==SOCKET_ERROR) 
	{  
      printf("send error!:%d\n",WSAGetLastError()); 
      return false; 
    } 
    else 
    printf("send ok!\n");
    closesocket(sock);    //关闭套接口,释放套接描述字 
    WSACleanup();   //解除与Socket库的绑定并且释放socket库所占的系统资源 
    return 1; 

}

总结

源码给大家做了很多注释,方便理解,已经非常非常简单易懂了,如果还有疑问可以留言或私信,希望能帮到大家。

发布了21 篇原创文章 · 获赞 2 · 访问量 3369

猜你喜欢

转载自blog.csdn.net/weixin_44164333/article/details/104688847