利用socket实现简单的文件传输小程序

时间:2022-01-28 16:23:15

1. 前言

最近老板突然说, 需要在原先的相机系统上面添加一个文件传输的模块, 不让用现成的软件如filezilla, 没办法只能自己手写一个

2. 实现效果

利用socket实现简单的文件传输小程序

3. 基本思想

  1. 本质上, 文件传输就是从一个地方读取二进制流, 并在另一个地方写入二进制流, 从而实现文件的传输工作。
  2. 因为我们对C++相对比较熟悉,所以这里选择利用C++ 实现文件传输的核心逻辑工作, 然后利用vb做界面封装
  3. 我们这里并没有引入线程池, socket池进行编程, 首先是我们这个项目并不需要这么高端的功能, 其次, 说到socket池, 一般想到的是linux 下的epoll编程, windows 下却没有, 也就懒得整了。

4. 核心代码

4.1 vb.net 界面代码

这个部分主要涉及:
1. 文件存在性检测 (使用fileInfo 进行存在性检测), 参考文章: http://blog.csdn.net/arrowzz/article/details/17144497
2. ip 地址有效性检测(正则表达式检测) 参考文章: http://blog.csdn.net/fengqingtao2008/article/details/7344906
3. 调用 C++ 的 dll 模块 参考文章: http://blog.csdn.net/zhyh1435589631/article/details/52181093

Imports System.Text.RegularExpressions
Imports System.IO
Public Class Form1
Private Declare Function myclient Lib "dll_client2.dll" (ByVal port As Integer, ByVal filename As String, ByVal ip As String) As Integer

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
// 文件存在性检测
Dim file As String = FileName.Text.ToString()
If Not checkFile(file) Then
MessageBox.Show("文件不存在,请重试")
Return
End If

// ip 有效性检测
Dim ip As String = IPADDR.Text.ToString.Trim
If ip = "" Then
ip = "127.0.0.1"
End If

If Not checkIP(ip) Then
MessageBox.Show("IP地址错误,请重试")
Return
End If

// 调用 C++ dll 模块实现文件传输
Dim ret As Integer = myclient(6000, file, ip)
If ret <> 0 Then
MessageBox.Show("传输失败,请重试")
Else
MessageBox.Show("传输成功")
End If
End Sub

// 支持拖拽
Private Sub FileName_DragDrop(sender As Object, e As DragEventArgs) Handles FileName.DragDrop
Dim MyFiles() As String
MyFiles = e.Data.GetData(DataFormats.FileDrop)
FileName.Text = MyFiles(0)
End Sub

Private Sub FileName_DragEnter(sender As Object, e As DragEventArgs) Handles FileName.DragEnter
e.Effect = DragDropEffects.All
End Sub

Private Function checkIP(ip As String)
Dim strPattern As String = "^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]).(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]).(\d{1,2}|1\d\d|2[0-4]\d|25[0-5]).(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$"
Return Regex.IsMatch(ip, strPattern)
End Function

Private Function checkFile(file As String)
Dim fFile As New FileInfo(file)
If Not fFile.Exists Then
Return False
Else
Return True
End If
End Function
End Class

4.2 C++ 客户端 dll 模块编写

  1. 需要注意的是, 这个导出的dll入口的名字, 生成可能不一样, 最好每次检查一下
  2. 这段代码核心, 是建立socket套接字, 连接服务器端, 发送文件名, 数据大小, 文件二进制流
  3. 需要注意的是, 发送文件名与发送文件二进制流之间需要有一定的时间间隔, 确保服务器端正确的将相应的文件建立起来

client.def

LIBRARY
EXPORTS
myclient = ?myclient@@YGHHPAD0@Z

client.h

#pragma once 

int __stdcall myclient(int port, char * filename1, char * ip);

client.cpp

#include <WinSock2.h>
#include <stdio.h>
#include <fstream>
#include <string>
#include <thread>
#include <iostream>
#include "client.h"

using namespace std;

#pragma comment(lib, "ws2_32.lib")

const int bufsize = 1024;

int __stdcall myclient(int port, char * filename1, char * ip){
string tmp(filename1);
int pos = tmp.rfind('\\');
if (pos >= 0){
tmp = tmp.substr(pos + 1);
}

char filename_buf[255] = { 0 };
strcpy(filename_buf, tmp.c_str());

WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
return -1;

if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1){
WSACleanup();
return -1;
}

SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr(ip);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(port);

int ret = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

// send filename

ret = send(sockClient, filename_buf, strlen(filename_buf) + 1, 0);
if (ret == -1){
cerr << "error" << endl;
return -1;
}

// send content
#define _CPP
#ifdef _CPP
string filename(filename1);
ifstream fd(filename, ifstream::binary);
fd.seekg(0, ifstream::end);


// size
Sleep(100);
char buf[bufsize] = { 0 };
int filesize = fd.tellg();
sprintf(buf, "%d", filesize);
send(sockClient, buf, strlen(buf) + 1, 0);

fd.seekg(0);
memset(buf, 0, bufsize);

Sleep(500);
while (fd.good()){
fd.read(buf, bufsize);
int count = fd.gcount();
ret = send(sockClient, buf, count, 0);
if (ret == -1){
cerr << "error" << endl;
return -1;
}
memset(buf, 0, bufsize);
}
cout << buf << endl;
#else
FILE * fd = fopen(filename_buf, "rb");
Sleep(500);
char buf[bufsize] = { 0 };

while (true){
int count = fread(buf, 1, bufsize, fd);
if (count <= 0)
break;

ret = send(sockClient, buf, count, 0);
if (ret == -1){
cerr << "error" << endl;
return -1;
}
memset(buf, 0, bufsize);
}
fclose(fd);

#endif
return 0;
}

4.3 C++服务器端

  1. 默认监听6000端口, 单线程工作模式, 每次只接受一个连接, 当这个链接的任务结束之后, 才会开始等待下一个连接的到来
  2. 在接收数据的子线程中, 首先接收文件名, 根据文件名创建文件, 然后接收文件大小, 根据文件大小, 控制线程是否接收完数据然后退出。
#include <WinSock2.h>
#include <stdio.h>
#include <fstream>
#include <string>
#include <thread>

//using namespace std;
using std::ofstream;
using std::string;
using std::thread;

#pragma comment(lib, "ws2_32.lib")

const int bufsize = 1024;

int myrecv(SOCKET sockConn){
char buffer[bufsize] = { 0 };
// filename
int ret = recv(sockConn, buffer, bufsize, 0);
if (ret == -1){
return -1;
}

#define _CPP
#ifdef _CPP
string filename(buffer);

recv(sockConn, buffer, bufsize, 0);
int count = atoi(buffer);

ofstream of(filename, ofstream::binary);
memset(buffer, 0, bufsize);
int sum = 0;
while ((ret = recv(sockConn, buffer, bufsize, 0)) > 0){
of.write(buffer, ret);
sum += ret;
memset(buffer, 0, bufsize);
if (sum >= count)
break;
}
#else
FILE * fd = fopen(buffer, "wb");
memset(buffer, 0, bufsize);
while ((ret = recv(sockConn, buffer, bufsize, 0)) > 0){
fwrite(buffer, 1, ret, fd);
memset(buffer, 0, bufsize);
}
fclose(fd);
#endif

return 0;
}

int main(int argc, char * argv[]){
int port = 6000;
if (argc == 2){
port = atoi(argv[1]);
}

WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
return -1;

if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1){
WSACleanup();
return -1;
}

SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(port);

bind(sockSrv, (SOCKADDR *)&addrSrv, sizeof(SOCKADDR));
listen(sockSrv, 5);

SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);

while (true){
SOCKET sockConn = accept(sockSrv, (SOCKADDR *)&addrClient, &len);
thread t1(myrecv, sockConn);
t1.join();
/*myrecv(sockConn);*/
}

return 0;
}

5. 遇到的一些问题

  1. 文件传输一定需要采用 二进制形式, 默认是 文本格式
  2. 原先采用dll 模块发送数据的时候, 服务器端线程不退出, 后来,通过预先传递文件大小的方式, 控制服务器端线程退出

6. 后序优化点

  1. 服务器端采用线程池, 支持并发传递数据
  2. 服务器端采用socket池(类似epoll机制), 提高效率
  3. 客户端如果传输大文件, 可以采用多线程方式, 将文件切割成多个部分进行传输