本文首发于稀土掘金。该平台的作者 逐光而行 也是本人。
theme: channing-cyan
highlight: ally-light
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天, 点击查看活动详情 这也是第7篇文章。
任务
设计一个基于UDP的文件下载工具,client可从server处下载其路径下的文件
编码前的前置背景知识
实现两端上传下载的步骤
server端
- 接收将被下载的文件名称,并将该名称发送给client;
- 当收到client的回复后,打开文件并读取内容,将读取的内容发送给client;
- 当文件所有内容发送完成后,给client发送一个上传完成的标识,关闭socket并退出。
client端
- 向server发送要下载的文件名称;
- 在本地创建该文件并确认开始接收文件;
3. 从server中接收文件的内容,并保存在创建好的文件中;
4. 接收完毕,关闭socket并退出。
调用操作系统为程序员提供的基于socket访问TCP/IP协议栈的编程接口
socket函数需要的头文件
#include <sys/types.h>
#include <sys/socket.h>
socket函数原型声明:
int socket(int domain, int type, int protocol);
其中:
domain
type
protocol
指定某个协议的特定类型,如果只有一种类型,就只能为0
代码中出现的sockaddr_in结构体
在编写程序时,通常约定:
使用结构体sockaddr_in来设置地址,然后通过强制类型转换成sockaddr类型。
两者各自的组成部分及转换关系如下图所示:
绑定端口
函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中:
- sockfd:已经创建的套接字;
- addr:是一个指向sockaddr参数的指针,其中包含了IP地址、端口;
- addrlen:addr结构的长度;
- 返回值:调用成功,返回值为0,否则返回-1,并设置错误代码errno。
注意:
如果创建套接字时使用的是AF_INET协议族,则addr参数所使用的结构体为struct sockaddr_in指针。
当设置addr参数的sin_addr为INADDR_ANY而不是某个确定的IP地址时,就可以绑定到任何网络接口。对于只有一个IP地址的计算机,INADDR_ANY对应的就是它的IP地址;对于有多个网卡的主机,INADDR_ANY表示本服务器程序将处理来自任何网卡上相应端口的连接请求。
代码及注释
server端代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
//定义数据块大小
char data[20000];
//定义端口号
int PORT=1234;
int download()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
printf("UDP套接字创建失败: %s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
memset(&addr,0,sizeof(addr))
//以上这两步都是将数组内元素清0,个人感觉可能打个或者号会好些
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("绑定端口失败: %s\n", strerror(errno));
return -1;
}
//存放客户端主机信息
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
int fd;
char filePath[100];
char ack[5];
//int len=sizeof(addr);
//getsockname(sockfd, (struct sockaddr *)&addr, &len);
//上述操作取出套接字,内含本地端通信用的ip和端口,有个问题问socket中有没有这些信息,其实是有的
//提示当前状态
//这个目前是静态的显示,有需要的可以稍加修改成对系统特定文件目录的扫描并显示可下载的文件
printf("本地端可供下载的文件【%s,%d】:1.txt",inet_ntoa(addr.sin_addr),PORT);
//接收要下载的文件名称
recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);
//data为下载文件的名称
sprintf(filePath, "%s", data);
printf("客户端【%s,%d】下载文件:%s\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port),filePath);
fd = open(filePath, O_RDONLY);
if(fd==-1)
{
data[0] = 'n';
sendto(sockfd, data, 1, 0, (struct sockaddr *)&clientAddr, clientAddrLen);
printf("文件不存在!\n");
close(fd);
close(sockfd); //及时关闭套接字,这一步很重要!
return -1;
}
//设置data类型为文件内容(c-content)
data[0] = 'c';
int readSize = 0;
//每次读取2000字节,最后一次不保证
while((readSize = read(fd, &(data[1]), 2000)) != 0)
{
//向客户端发送数据
sendto(sockfd, data, readSize+1, 0, (struct sockaddr *)&clientAddr, clientAddrLen);
//等待客户端接收确认
recvfrom(sockfd, ack, sizeof(ack), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);
//清空缓存,等待下一波2000字节
memset(&(data[1]), 0, sizeof(data)-1);
}
//表示已经读到文件尾(e-end)
data[0] = 'e';
sendto(sockfd, data, 1, 0, (struct sockaddr *)&clientAddr, clientAddrLen);
close(fd);
close(sockfd); //关闭socket
return 0;
}
int main(int argc, char* argv[])
{
if(argc>2) {printf("用法:uploadFileServer [端口]\n"); return -1;}
if(argc==2) PORT = atoi(argv[1]);
download();
return 0;
}
client端代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
char data[20000];
int PORT=1234;
char* SERVER_IP="127.0.0.1";
int main(int argc, char *argv[])
{
int sockfd;
//client端用户指定的下载文件的名称
char *downLoadFileName;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
return -1;
}
if(argc!=2 && argc!=4) {printf("用法:uploadFileClient [IP 端口] >文件名\n"); return -1;}
if(argc==4) {
SERVER_IP = argv[1];
PORT = atoi(argv[2]);
downLoadFileName=argv[3];
}
else {downLoadFileName = argv[1];};
struct sockaddr_in servAddr;
int servAddrLen = sizeof(servAddr);
bzero(&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(PORT);
servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
//在本地创建要下载的文件
int fd = open(downLoadFileName, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
//向server端发送要下载的文件名称
sendto(sockfd, downLoadFileName, strlen(downLoadFileName)+1, 0, (struct sockaddr*)&servAddr, servAddrLen);
int recvLen;
//接收服务器发来的数据
while((recvLen = recvfrom(sockfd, data, sizeof(data), 0, (struct sockaddr *)&servAddr, &servAddrLen)) > 0)
{
if(data[0] == 'c')
{
write(fd, &(data[1]), recvLen - 1);
//确认接收
sendto(sockfd, "OK", strlen("OK"), 0, (struct sockaddr *)&servAddr, servAddrLen);
memset(data, 0, sizeof(data));
}
else
{
if(data[0]=='n') printf("文件不存在\n");//(n-none)
if(data[0]=='e') printf("文件下载完毕\n");
close(fd); //关闭套接字
break;
}
}
close(sockfd);
return 0;
}
注意事项
在C/S模型中,必须先启动server,再启动client。
参考资料
计算机网络课程实验:linux下的UDP通信程序设计