Socket介绍及C实现TFTP客户端


一、Socket套接字

因为TFTP客户端的实现需要用到Socket套接字,所以本文先对套接字进行一个简单介绍。

1.什么是Socket套接字

网络应用程序是由通信进程对组成,每对互相通信的应用程序进程互相发送报文,他们之间的通信必须通过下面的网络来进行。为了将应用程序和底层的网络通信协议屏蔽开来,采用套接字(Socket)这样一个抽象概念来作为应用程序和底层网络之间的应用程序编程接口(API)。

因为网络应用程序是进程之间的通信,为了唯一的标识通信对等方的通信进程,套接字必须包含2种信息:(1) 通信对等方的网络地址。(2) 通信对等方的进程号,通常叫端口号。

而在Windows操作系统下,提供的一套网络通信协议的开发接口,称为Windows Sockets或简称Winsock。

Winsock 是通过动态链接库的方式提供给软件开发者,而且从Windows 95以后已经被集成到了Windows操作系统中。
Winsock主要经历了2个版本:Winsock 1.1和Winsock 2.0。Winsock 2.0是Winsock 1.1的扩展,它向下完全兼容。
Winsock同时包括了16位和32位的编程接口,16位的Windows Socket 2应用程序使用的动态链接库是WINSOCK.DLL,而32位的Windows Socket应用程序使用WSOCK32.DLL(Winsock 1.1版)和WS2_32.DLL(Winsock 2.0版)。另外,使用Winsock API时要包含头文件winsock.h(Winsock 1.1版)或winsock2.h(Winsock 2.0版)。

2.Socket的两种类型

Socket是一个抽象概念,代表了通信双方的端点(Endpoint),通信双方通过Socket发送或接收数据。

在Winsock里,用数据类型SOCKET作为Windows Sockets对象的句柄,就好像一个窗口的句柄HWND、一个打开的文件的文件指针一样。
而Socket主要有2种类型:

- 流类型(Stream Sockets)。
流式套接字提供了一种可靠的、面向连接的数据传输方法,使用传输控制协议TCP。
- 数据报类型(Datagram Sockets)。
数据报套接字提供了一种不可靠的、非连接的数据包传输方式,使用用户数据报协议UDP。

3.Socket I/O的2种模式

一个SOCKET句柄可以看成代表了一个I/O设备。在Windows Sockets里,有2种I/O模式:

- 阻塞式I/O(blocking I/O)
在阻塞方式下,收发数据的函数在调用后一直要到传送完毕或者出错才能完成,在阻塞期间,除了等待网络操作的完成不能进行任何操作。阻塞式I/O是一个Winsock API函数的缺省行为。

- 非阻塞式I/O(non-blocking I/O)
对于非阻塞方式,Winsock API函数被调用后立即返回;当网络操作完成后,由Winsock给应用程序发送消息(Socket Notifications)通知操作完成,这时应用程序可以根据发送的消息中的参数对消息做出响应。Winsock提供了2种异步接受数据的方法:一种方法是使用BSD类型的函数select(),另外一种方法是使用Winsock提供的专用函数WSAAsyncSelect()。

4.流式套接字用法

由于流式套接字使用的是基于连接的协议,所以你必须首先建立连接,而后才能从数据流中读出数据,而不是从一个数据报或一个记录中读出数据。

主要流程:
流式套接字

5.数据报套接字

首先,客户机和服务器都要创建一个数据报套接字。接着,服务器调用bind()函数给套接字分配一个公认的端口。一旦服务器将公认的端口分配给了套接字,客户机和服务器都能使用sendto和revfrom来传递数据报。通信完毕调用closesocket()函数来关闭套接字。
主要流程:
数据报套接字

6.主要使用函数

Socket编程主要使用库函数在winsock2.h头文件下
函数的更多具体说明可查看另一篇文章:套接字编程部分库函数


二、TFTP客户端实现

1.什么是TFTP

这部分具体内容可查看另一篇文章:TFTP协议介绍

2.客户端结构及功能设计

客户端设计

3.客户端主要操作

主要操作操作说明:
1.发送读/写请求模块,主要使用sendRequest函数进行请求指令的填充,并用sendto函数进行指令的发送。
2.在接收报文时,使用recvfrom函数进行接收,并通过特定标志位来确定是否传输正确或发生错误。
3.在发送ACK时使用sendack函数来进行ACK的填充和报文发送,发送DATA段时,直接使用sendto函数。
4.在上传函数当中,若无法接收到ACK便会进行重传。

4.部分函数展示

部分定义

#define BUFFER_SIZE 1024      //块大小
#define READ_REQUEST 1        //读请求
#define WRITE_REQUEST 2       //写请求
#define DATA 3                //数据
#define ACKNOWLEDGEMENT 4     //ACK
#define WRONG_RECV 5          //错误

#define RECV_LOOP_MAX 6	  //超时最大重发次数
#define TIME_OUT_SEC 5	      //超时时间

建立连接:

while (1)//建立连接
{
    
    
	system("cls");
	cout << "请输入服务器IP:";
	char ip[20];
	cin >> ip;
	cout << "请输入服务器端口号:";
	int port;
	cin >> port;
	addr.sin_family = AF_INET;
	addr.sin_addr.S_un.S_addr = inet_addr(ip);
	addr.sin_port = htons(port);
	sServSock = socket(AF_INET, SOCK_DGRAM, 0);//建立scoket
	if (sServSock == INVALID_SOCKET)
		cout << "连接失败";
	else
		break;
}

发送读/写请求:

int sendRequest(int op, const char* filename, int type, FILE* fp, SOCKET sServSock, sockaddr_in addr, int addrLen) {
    
     //type = 1,文本文件,type = 2,二进制文件
	char buf[20]; //数据报文
	if (op == READ_REQUEST)
		fprintf(fp, "请求下载数据:%s\n", filename);
	else
		fprintf(fp, "请求上传数据:%s\n", filename);
	memset(buf, 0, sizeof(buf));        //清空数组
	buf[1] = op;                     //前两个字节存放操作数,因为只有0/1,一个字节就够
	strcpy(buf + 2, filename);       // 之后的字节存放文件名
	if (type == 2)
	{
    
    
		strcpy(buf + 8, "octet");        			//octet传输二进制数据(6+2=8)
		return sendto(sServSock, buf, 14, 0, (LPSOCKADDR)&addr, addrLen);	
		//发送数据报文(8+6=14)
	}
	else
	{
    
    
		strcpy(buf + 8, "netascii");										                                                      		
		//netascii传输字符数据
		return sendto(sServSock, buf, 17, 0, (LPSOCKADDR)&addr, addrLen);	
		//发送数据报文(8+9=17)
	}
}

发送ACK:

void sendACK(int num, FILE* fp, SOCKET sServSock, sockaddr_in addr, int addrLen)
{
    
    
	char buf[4];
	memset(buf, 0, 4);
	buf[1] = ACKNOWLEDGEMENT;
	memcpy(&buf[3], &num, 1);
	int t = num >> 8;
	memcpy(&buf[2], &t, 1);
	sendto(sServSock, buf, 4, 0, (LPSOCKADDR)&addr, addrLen);
	printf("sendACK=%d\n", num);
}

接收报文:

int recvfrom1(SOCKET fd, char recvbuf[], size_t buf_n, sockaddr* addr, int* len, const char sendbuf[], int sendbufsize, FILE* fp,int blocknum )
{
    
    
	struct timeval tv;
	fd_set readfds;
	int n = 0;
	for (int i = 0; i < RECV_LOOP_MAX; i++) {
    
    
		FD_ZERO(&readfds);
		FD_SET(fd, &readfds);
		tv.tv_sec = TIME_OUT_SEC;
		tv.tv_usec = 0;
		select(fd + 1, &readfds, NULL, NULL, &tv);
		if (FD_ISSET(fd, &readfds)) {
    
    
			if ((n = recvfrom(fd, recvbuf, buf_n, 0, addr, len)) >= 0) {
    
    
				return n;
			}
		}
		if (blocknum == 0)
			return -2;
		//重传内容
		sendto(fd, sendbuf, sendbufsize, 0, addr, *len);
		printf("超时重传:sendBlockNum=%d!\n", blocknum);
	}
	return -1;
}

5.运行截图

日志
运行截图


总结

因为这个小程序只是当时四节课的一个小实验,所以并没有进行很复杂的操作和图形界面的设计,目的主要是了解协议和套接字的具体实现。之后我也会将源码发至资源。
有任何问题都可以在评论区和我交流~~

猜你喜欢

转载自blog.csdn.net/qq_45740212/article/details/113104021