源码下载:
在开始做调试助手前,首先要明确的就是我们所需要做出来的功能。所以参考了一下网络上面其他人做的调试助手,需要的功能如下:
1:能够通过TCP协议进行客户端和服务器之间的通信。
2:能够通过UDP协议来实现两台主机之间的通信。
3:可以在界面上显示IP地址,方便在我们使用TCP服务器端的时候来绑定地址。
4:界面有数据接收区和数据发送区两个编辑框,用于发送和接收数据。
5:在接收区和发送区有两个多选框,用于发送和接收16进制的数据。
6:可以实现定时发送的功能。
7:显示已经发送的数据的字节数和已经接收数据的字节数,并设置一个按钮使计数器清0。
下面将说一下使用TCP协议和UDP协议通信的原理:(VC++)
TCP协议
TCP协议是一种面向连接的,可靠的,基于字节流的传输层通信协议。而采用TCP协议的主机之间通信时,主机可以分成客户端和服务器两种类型。下图描述了客户端与服务器之间通信的流程:
服务器
(1):创建一个socket:
m_serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if(m_serverSocket == INVALID_SOCKET)
{
MessageBox("服务器: 创建套接字失败", "提示");
return;
}
AF_INET:使用IPv4网络协议进行通信。
SOCK_STREAM:选择使用面向连接的套接字。
0:不指定套接口所用的协议。
如果创建失败,返回INVALID_SOCKET。
(2):给创建的socket绑定本机的地址和进程所占用的端口号:
u_short port = atoi(strPort);
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_addr.S_un.S_addr = htonl(m_numberIP);
addrServer.sin_port = htons(port);
int addrBind;
addrBind = bind(m_serverSocket,(SOCKADDR *)&addrServer, sizeof(addrServer));
if(addrBind == SOCKET_ERROR)
{
MessageBox("绑定地址失败", "提示");
return;
}
(3):使服务器进入监听状态:
int clientListen;
clientListen = listen(m_serverSocket, 5);
if(clientListen == SOCKET_ERROR)
{
MessageBox("监听失败", "提示");
return;
}
在这里,listen函数参数中得5代表的是套接字监听听队列中的最大值。
(4):接收远程主机的连接:accept()
在MFC下套接字默认使用的是同步模式,所以当我们使用accept()函数的时候,线程会进入阻塞状态,直到客户端的connect()请求来临的时候才会返回。如果就这样在界面直接使用accept()函数的时,界面将会进入假死状态,这个状态类似于在我们打开网页或者玩游戏的时候出现的未响应状态。显然这是我们不想要的结果,解决方法:可以将accept放入一个子线程中去,这样在使用accept()函数的时候界面就不会进入假死状态了。首先我们需要创建一个单独的线程用来一直接收远程主机的连接请求:
HANDLE hThread = CreateThread(NULL, 0, ServerRecvProc, (LPVOID)pRecvParam, 0, NULL);
CloseHandle(hThread);
pRecvParam是我们在父线程中向子线程传递的参数,这个参数通常是一个结构体类型的指针。ServerRecvProc则表示是我们创建的线程的名字。
线程函数如下:
DWORD WINAPI CNetworkSendDataDlg::ServerRecvProc(LPVOID lpParam)
{
HWND hwnd = ((RECVPARAM *)lpParam)->hwnd;
CNetworkSendDataDlg *pDlg = ((RECVPARAM *)lpParam)->pDlg;
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
pDlg->m_clientSocket = accept(pDlg->m_serverSocket, (SOCKADDR *)&addrClient, &len);
if(pDlg->m_clientSocket != INVALID_SOCKET)
{
//绿灯亮
pDlg->m_Led.LoadBitmaps(IDB_BITMAP_GREEN);
pDlg->m_Led.Invalidate();
CString ipConnect;
ipConnect.Format("连接到 IP: %s Port: %d", inet_ntoa(addrClient.sin_addr), ntohs(addrClient.sin_port));
pDlg->SetDlgItemText(IDC_STATIC_CONNECT, ipConnect);
HANDLE handle = CreateThread(NULL, 0, MonitorThread, (LPVOID)lpParam, 0, NULL);
CloseHandle(handle);
}
else
{
return 0;
}
//初始化数组
u_char recvBuf[MAXSIZE] = {0};
CString displayBuf;
int recvLength;
while(TRUE)
{
recvLength = recv(pDlg->m_clientSocket, (char *)recvBuf, MAXSIZE, 0);
if(recvLength == SOCKET_ERROR)
break;
pDlg->m_recvCounter += recvLength;
if(pDlg->m_16display.GetCheck() == 1)
{
char outData[2*MAXSIZE] = {0};
pDlg->Number16_To_Str16(recvBuf, outData, recvLength);
displayBuf.Format("%s (%d)\r\n", outData, recvLength);
}
else
{
recvBuf[recvLength] = '\0';
displayBuf.Format("%s", recvBuf);
}
::PostMessage(hwnd, WM_RECVDATA, 0, (LPARAM)displayBuf.GetBuffer(0));
}
return 0;
}
Accept()函数的返回值是一个套接字,通过它我们可以知道远程主机的IP地址和端口号。
(5):接收和发送数据
在TCP协议中,我们可以使用send来发送数据,使用recv来接收数据,但是如果套接字是同步模式的话,recv函数也会像accept函数一样进入阻塞状态,直到远程主机的send来临后才会返回。所以我们也将recv放入一个单独的线程里面,在这里我将这个recv放在了刚刚accept函数所在的线程里面。代码如下:
recvLength = recv(pDlg->m_clientSocket, (char *)recvBuf, MAXSIZE, 0);
recv函数的返回值为接收到字节的个数。
发送数据时,我们可以使用send函数,实现如下:
if(send(m_clientSocket, (char *)outData, dataLength, 0) != SOCKET_ERROR)
m_sendCounter += dataLength;
客户端
(1):在客户端中,首先我们先要创建一个TCP套接字,代码如下:
m_clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if(m_clientSocket == INVALID_SOCKET)
{
MessageBox("客户端: 创建套接字失败", "提示");
return;
}
(2):在这里,我们要定义一个SOCKADDR_IN类型的变量,需要将远程主机的IP地址,端口号和所采用的的协议族赋值给这个结构体,以保证connect函数的发送位置,代码如下:
DWORD addressIP;
m_IPAddress.GetAddress(addressIP);
CString strPort;
GetDlgItemText(IDC_EDIT_CLIENTPORT, strPort);
u_short port = atoi(strPort);
if(strPort.IsEmpty() || addressIP == 0)
{
MessageBox("请设置IP地址或端口号");
return;
}
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_addr.S_un.S_addr = htonl(addressIP);
addrServer.sin_port = htons(port);
atoi( ):将字符串转换为unsigned short类型的数。
htonl( ):将一个unsigned long类型的数据由主机排序方式转换为网络排序方式。
htons( ):将一个unsigned short类型的数据由主机排序方式转换成网络排序方式。
(3):使用connect函数连接到远程主机,代码如下:
if(connect(m_clientSocket, (SOCKADDR *)&addrServer, sizeof(addrServer)) == SOCKET_ERROR)
{
MessageBox("连接超时", "提示");
SetDlgItemText(IDC_STATIC_CONNECT, "未连接");
return;
}
else
{
m_Led.LoadBitmaps(IDB_BITMAP_GREEN);
m_Led.Invalidate();
CString ipConnect;
ipConnect.Format("连接到 IP: %s Port: %d", inet_ntoa(addrServer.sin_addr), ntohs(addrServer.sin_port));
SetDlgItemText(IDC_STATIC_CONNECT, ipConnect);
}
inet_nota( ):将一个按网络排序方式的无符号长整型数据转换为字符串。
ntohs( ):将一个unsigned short类型的数据由网络排序方式转换为主机排序方式。
(4):一旦客户端连接到远程服务器,它们之间就可以互相通信了(可以互相send和recv),send,recv部分,客户端和服务器的原理是基本相同的。
TCP协议的三次握手:
TCP协议是一种可靠的协议,为双方通信提供了可靠的数据,TCP协议建立连接前需要进行三次握手,每一次握手如下:
(1):建立连接时,客户端发送syn包(syn = j)到服务器,并进入SYN_SENT状态,等待服务器确认。
(2):服务器接收到syn包,为了回应客户端的连接请求,自己也回应一个syn包(syn = k)至客户端,即syn + ack包,此时服务
器进入SYN_RECV状态。
(3):客户端收到服务端的确定请求后(syn + ack包),向服务器发送确定包ACK(ack = k+1),这个包发送完毕后,客户端进入
ESTABLISHED状态,服务器接收到这个包后也将进入ESTABLISHED状态,完成了三次握手,此时TCP就连接成功了。
TCP协议三次握手的更多细节可以参考下面这个博客:
UDP协议
UDP协议全称是用户数据报协议[1] ,在网络中它与TCP协议一样用于接收和发送数据包,是一种无连接的协
议。
由于UDP协议并没有像TCP那样连接前有三次握手和重传的机制,所以通过UDP协议发送的数据的正确性是不敢
保证的,但是正因为如此,UDP协议较TCP协议更加方便。所以在对数据的完整性的要求不是很高的场合下,使用
UDP协议更加简单。例如:我们平常所使用的QQ在聊天之间的信息传输就是基于UDP协议的。
在UDP协议里面没有TCP协议中服务器与客户端的概念,但是在两台利用UDP进行通信的主机之间,我们可以近
似的把接收数据的一端叫做服务器,发送数据的一端叫做客户端,这样便于我们的理解。
下图说明了两个主机之间利用UDP通信的基本流程:
(1):创建套接字:创建UDP套接字的时候,我们需要规定使用数据包的格式,所以在第二个参数中写上
SOCK_DGRAM。
m_serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
if(m_serverSocket == INVALID_SOCKET)
{
MessageBox("UDP: 创建套接字失败", "提示");
return;
}
(2):给套接字绑定IP地址,端口号和协议族。
//在套接字上绑定地址
int addrBind;
addrBind = bind(m_serverSocket,(SOCKADDR *)&addrServer, sizeof(addrServer));
if(GetLastError() == WSAEADDRINUSE)
{
MessageBox("这个端口或地址已被其他应用程序占用", "提示");
}
else if(addrBind == SOCKET_ERROR)
{
MessageBox("绑定地址失败", "提示");
return;
}
(3):发送和接收数据:在UDP协议中发送和接收数据分别是使用sendto和recvfrom两个函数来实现的。
sendto:
SOCKADDR_IN addrClient;
addrClient.sin_family = AF_INET;
addrClient.sin_addr.S_un.S_addr = htonl(m_numberIP);
addrClient.sin_port = htons(atoi(strPort));
if(sendto(m_serverSocket, (char *)outData, dataLength, 0, (SOCKADDR *)&addrClient, sizeof(addrClient))
!= SOCKET_ERROR)
m_sendCounter += dataLength;
m_serverSocket:表示通过本机所创建的这个套接字来与远程主机实现通信。
strSend:是所发送的数据。
0:标志位,0表示无任何其他的功能。
addrClient:表示一个地址簇,将远程主机的IP地址,端口号和协议族赋值进这个结构体中。然后在sendto中就可以通过这一地址信息将数据传输到目的主机中。
recvfrom:
recvLength = recvfrom(pDlg->m_serverSocket, (char *)recvBuf, MAXSIZE, 0, (SOCKADDR *)&addrSocket, &len);
if(recvLength == SOCKET_ERROR)
break;
recvfrom会将接收到的数据存放在recvBuf数组中,远程主机的地址簇存放在addrSocket结构体中,函数返回
接收到的数据的长度,若接收错误将会返回SOCKET_ERROR。
在界面上显示IP地址
在MFC中我们可以添加一个编辑框的控件来显示本机的IP地址,要获得IP地址,我们可以使用gethostname( )和
gethostbyname( )两个函数来实现这一功能,代码如下:
int CNetworkSendDataDlg::ShowIpAddress(void)
{
/* #define MAX_PATH 260 */
char hostname[MAX_PATH] = {0};
gethostname(hostname,MAX_PATH);
hostent* ptent = gethostbyname(hostname);
if (ptent != NULL)
{
char *ip = inet_ntoa(*(in_addr *)ptent->h_addr_list[0]);
m_numberIP = ntohl(inet_addr(ip));
m_EditAddress.SetWindowText(ip);
}
return 0;
}
gethostname():我们可以通过这个函数将主机名存入hostname所指向的缓存区中,MAX_PATH为缓存区的长度。
gethostbyname():返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针。
接收区和发送区
(1):在界面添加两个编辑框的控件,用来接收和发送数据。
(2):在接收区和发送区旁边添加两个多选按钮,用于显示和发送16进制的数据。
在这里输入的时候需要我们注意以下三个问题:
1:在编辑框中数据是按照字符存储的,所以我们需要定义一个把字符转换为相应的16进制数据的函数。比如
我们输入61,意思是我们想要发送ASCII码为97的字符,而实际上在编辑框中是储存了’6’和’1’两个字符,如果这
样直接发送就达不到发送16进制的功能了。
2:由于每隔一个字节,我们要输入一个空格,而这个空格也属于字符,所以我们需要先将编辑框字符中的空
格字符除去后再转换成相应的16进制数据。
3:在16进制中,它的输入范围是0-F,所以规定我们只能输入0-9,a-f,A-F里面的数据,如果输入超出了这个范
围,就需要提示输入错误。
以下就是解决以上这些问题的函数代码:
int CNetworkSendDataDlg::Str16_To_Number16(char *inData, u_char *outData, int Len)
{
char str[MAXSIZE] = {0};
int k = 0;
//除去输入中的空格符和换行符
for(int i = 0; i < Len; i++)
{
while(inData[i] == ' ' || inData[i] == '\t')
i++;
//末尾是空格的情况下
if(i == Len)
break;
//判断是否有非法字符
if((inData[i] >= '0' && inData[i] <= '9') || (inData[i] >= 'a' && inData[i] <= 'f')
|| (inData[i] >= 'A' && inData[i] <= 'F'))
str[k++] = inData[i];
else
{
MessageBox("输入中存在非法字符(输入的字符中只能包含0-9,a-z, A-Z中的字符)", "提示");
m_EditSend.SetSel(i, i+1);
return -1;
}
}
if((k % 2) != 0)
{
MessageBox("输入的数据是没有完全两两配对", "提示");
return -1;
}
for(int j = 0; j < (k / 2); j++)
{
char temp[2] = {str[2*j], str[2*j+1]};
//将两个十六进制的字符转换成相应的数值
sscanf(temp, "%x", &outData[j]);
}
//返回发送数据的字节数
return (k / 2);
}
sscanf_s %x是将temp中的字符转换成相应的16进制,由于temp只有两个字节,刚好转换成一个字节的16进
制数据。
显示16进制的数据:
当我们按照16进制的格式发送的时候,在接收区的编辑框中,由于显示的是16进制对应的字符,如果不加处
理直接显示的话,那么将会产生乱码。所以我们需要定义一个函数用来显示发送的16进制的数据。
这个函数的功能是将16进制的数据转换成相应的字符,为了接收区的美观,在每一个字节之间都加上一个空
格符,并在每一次接收数据后换行。转换函数如下:
int CNetworkSendDataDlg::Number16_To_Str16(u_char * inData, char *outData, int Len)
{
int j = 0;
for(int i = 0; i < Len; i++, j++)
{
if(inData[i] < 16)
{
outData[j++] = '0';
sprintf_s(&outData[j++], 2*MAXSIZE, "%x", inData[i]);
}
else
{
sprintf_s(&outData[j], 2*MAXSIZE, "%x", inData[i]);
j += 2;
}
outData[j] = ' ';
}
outData[j] = '\0';
for(int i = 0; i < j; i++)
{
if(outData[i] >= 'a' && outData[i] <= 'z')
outData[i] -= 32;
}
return 0;
}
定时发送:
在客户端或者服务器每一次进行连接的时候,单独创建一个线程,将手动发送的按钮设置为不可用,再利用sleep来实现定时
发送的功能。代码如下:
DWORD WINAPI CNetworkSendDataDlg::TimeSendFuntion(LPVOID lpParam)
{
CNetworkSendDataDlg *pDlg = ((RECVPARAM *)lpParam)->pDlg;
CButton *pt = (CButton *)pDlg->GetDlgItem(IDC_BUTTON_SEND);
CString strTime;
int numberTime;
while(1)
{
if(pDlg->m_CheckSend.GetCheck() == 1)
{
pt->EnableWindow(FALSE);
if(pDlg->m_CheckTimeSend.GetCheck() == 1)
{
pDlg->GetDlgItemText(IDC_EDIT_SENDTIME, strTime);
strTime += '\0';
numberTime = pDlg->strToNumber((char *)strTime.GetBuffer(0));
pDlg->OnBnClickedButtonSend();
Sleep(numberTime);
}
}
CString str1, str2;
pDlg->GetDlgItemText(IDC_STATIC_CONNECT, str1);
str2.Format("%s", "未连接");
if(str1 == str2)
{
pt->EnableWindow(TRUE);
break;
}
if(pDlg->m_CheckSend.GetCheck() == 0)
pt->EnableWindow(TRUE);
}
return 0;
}
显示发送和接收的字节数:我们可以在对话框类中定义两个计数变量,并在初始化的时候给它们赋值为0。
单独创建一个线程,用于刷新计数器的值。然后在每一次send和recv的时候给各自加上发送数据的字节数。
对于计数器清0,我们只需要添加一个按钮控件,将两个计数变量赋值为0就行了。
以上,所有的步骤就基本完成了,最后界面如下: