前言
前阵子我下载了基于tcp协议的客户端和服务器的代码,想借用一下来处理我们公司软件的日志模块,现在模块没问题了,但是我又想,服务器一般都在linux下,所以又在linux环境下写了一个服务器,也优化了客户端. 期间遇到了一些问题,都记录在这里. 如果哪里有问题,请大家指出,共同进步!
软件名称: 文件服务器
软件功能: 用于局域网内的文件传输
技术涉及:socket网络编程,tcp协议,文件操作,c/c++
软件不足:单线程,局域网,封装度低,数据结构不够精简
未来拓展:多线程,文件夹传输,提高接口封装性,数据结构优化
服务器代码
本来想用纯c写,但是消息体Message用的是类,所以加了头文件<iostream>,后面的话,打算把所有的结构全部封装进对象管理类里,让程序可读性更高,也更精简.
服务器端只完成了接受客户端上传文件,还未支持从服务器端下载文件的功能,大家可以手动添加一下
fileserver.cpp
//fileserver.cpp
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "message.h"
#define SERVERIP "这里写你服务器端的主机IP"
#define PORT 10001
#define MAX_FILE_SIZE 256
#define MAX_BUF_SIZE 1024*10
using std::cout;
using std::endl;
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
char gFileName[256];
int gFileLength;
/function declare
void ProcessConnect(int);
bool RecvFile(int,__MsgHead*);
/fucntion define/
bool RecvFile(int client_sock,__MsgHead* Msg){
cout << "=======================================Enter Function:" << __FUNCTION__ << endl;
//Start to receive file data
FILE* pFile = fopen(gFileName,"w");
if(!pFile){
perror("fopen");
return false;
}
char buf[MAX_BUF_SIZE];
unsigned int i = 0;
while(i < gFileLength - 1)
{
int recv_size = recv(client_sock,buf,MAX_BUF_SIZE,0);
if(recv_size < 0){
perror("recv_");
}
fwrite(buf,sizeof(char), recv_size,pFile);
i += recv_size;
memset(buf,0,sizeof(char)*MAX_BUF_SIZE);
}
fclose(pFile);
return true;
}
void ProcessConnect(int new_sock){
cout << "=======================================Enter Function:" << __FUNCTION__ << endl;
//完成一次连接的处理
//需要循环的来处理客户端发送的数据
char buf[MAX_BUF_SIZE];
while(1){
memset(buf,0,sizeof(buf));
//a)从客户端读取数据
int recv_size = recv(new_sock,buf,sizeof(buf),0);
if(recv_size == -1)
{
perror("recv__");
return;
}
if(recv_size == 0){
printf("[client %d] disconnect!\n", new_sock);
}
__MsgHead* msgHead = (__MsgHead*)buf;
//b)根据消息ID 响应客户端消息
switch(msgHead->msgId){
case MSG_FILE:
{
cout << "=======================================MSG_FILE" << endl;
if(!RecvFile(new_sock,(__MsgHead*)buf))
{
printf("receive file falied\n");
return;
}
}break;
case MSG_FILE_NAME:
{
cout << "=======================================MSG_FILE_NAME" << endl;
__MsgFileName* msg;
msg = (__MsgFileName*)msgHead;
strcpy(gFileName,msg->fileName);
printf("The file name that has been received is:%s\n",gFileName);
}
break;
case MSG_FILE_LENGTH:
{
cout << "=======================================MSG_FILE_length" << endl;
__MsgFileLength* msg;
msg = (__MsgFileLength*)msgHead;
printf("The length of the file is %lld bytes\n",msg->fileLength);
}
break;
case MSG_SEND_FILE:
{
cout << "=======================================MSG_SEND_FILE" << endl;
__MsgSendFile* msg = (__MsgSendFile*)msgHead;
printf("[client %d] prepare to receive file from client\n", new_sock);
}
break;
case MSG_DOWNLOAD_FILE:
{
cout << "=======================================MSG_download_FILE" << endl;
printf("client wants to download files\n");
}
break;
};
}
}
int main(int argc, char* argv[]){
//1.创建 socket
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0){
perror("socket");
return 1;
}
//2.绑定端口号
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(SERVERIP);
server.sin_port = htons(PORT);
int n;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
int ret = bind(listen_sock, (sockaddr*)&server, sizeof(server));
if(ret < 0){
perror("bind");
return 1;
}
//3.使用 listen 允许服务器被客户端连接
ret = listen(listen_sock, 5);
if(ret < 0){
perror("listen");
return 1;
}
//4.服务器初始化完成,进入事件循环
printf("Server Init OK!\n");
while(1){
//与客户端建立连接
sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (sockaddr*)&peer, &len);
if(new_sock < 0){
perror("accept");
continue;
}
printf("[client %d] connect!\n", new_sock);
ProcessConnect(new_sock);
close(new_sock);
sleep(1);
}
close(listen_sock);
return 0;
}
在消息体结构里,原来的结构看起来优点臃肿,其实完全可以用一个简单的类来表示消息体, 因为c++类的成员变量是按声明顺序初始化的,所以规定第一个变量表示消息头,后面的变量再指定文件属性,客户端每次send时,比如发文件名时,赋值fileName,而其他的变量置空,这样代码就简洁明了了. 因为linux没有_int64,所以定义了一个.
message.h
//message.h
#define MAX_PACK_SIZE 10240 //数据包的长度
#define MAX_FILE_NAME_LENGTH 256 //文件名的长度
#define INVALID_MSG -1 //无效的消息
#define MSG_FILE_LENGTH 1 //文件长度
#define MSG_FILE_NAME 2 //文件名
#define MSG_FILE 3 //文件内容
#define MSG_READY 4 //准备好消息
#define MSG_SEND_FILE 5 //发送文件
#define MSG_DOWNLOAD_FILE 6 //下载文件
#define MSG_COMPLETE 7 //完成信息
typedef long long _int64;
class Message{
public:
struct MsgHead //头消息
{
int msgId; //消息标识
MsgHead(int msg=INVALID_MSG):msgId(msg){};
};
struct MsgFileLength :public MsgHead
{
_int64 fileLength; //文件长度
MsgFileLength():MsgHead(MSG_FILE_LENGTH){}
};
struct MsgFileName:public MsgHead
{
char fileName[MAX_FILE_NAME_LENGTH];
MsgFileName():MsgHead(MSG_FILE_NAME){}
};
struct MsgFile:public MsgHead
{
MsgFile():MsgHead(MSG_FILE){}
};
struct MsgReady:public MsgHead //准备好消息
{
MsgReady():MsgHead(MSG_READY){}
};
struct MsgSendFile:public MsgHead //发送文件消息
{
MsgSendFile():MsgHead(MSG_SEND_FILE){}
};
struct MsgDownLoadFile:public MsgHead //下载文件消息
{
MsgDownLoadFile():MsgHead(MSG_DOWNLOAD_FILE){}
};
struct MsgComplete:public MsgHead
{
MsgComplete():MsgHead(MSG_COMPLETE){}
};
};
typedef Message::MsgHead __MsgHead; //Id ,msgId
typedef Message::MsgFileLength __MsgFileLength;//长度 ,fileLength
typedef Message::MsgFileName __MsgFileName; //文件名,fileName
typedef Message::MsgFile __MsgFile;
typedef Message::MsgReady __MsgReady;
typedef Message::MsgSendFile __MsgSendFile;
typedef Message::MsgDownLoadFile __MsgDownLoadFile;
typedef Message::MsgComplete __MsgComplete;
客户端在vs 2010专业版里可以正常运行
client.cpp
//client.cpp
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include <iostream>
#include <vector>
#include <WinSock2.h>
#include <string>
#include "client.h"
#include "message.h"
#include <tchar.h>
using namespace std;
int main()
{
Client client;
if(!client.InitSock())
{
cout<<"初始socket失败"<<endl;
return -1;
}
SOCKET saRemote=client.ConnectServer(client.ResolveAdress(SERVER_IP),PORT);
if(saRemote==INVALID_SOCKET)
{
cout<<"连接服务器失败"<<endl;
return -1;
}
if(!client.ProcessConnection(saRemote))
{
return -1;
}
client.CloseSocket();
return 0;
}
bool Client::InitSock() //初始socket
{
WSADATA wsData;
WORD wr=MAKEWORD(2,2);
if(WSAStartup(wr,&wsData)==0)
{
return true;
}
return false;
}
u_long Client::ResolveAdress(char *serverIp) //解析IP地址
{
u_long nAddr=inet_addr(serverIp);
if(nAddr==INADDR_NONE) //表明serverIp使用的是主机名形式
{
hostent *ent=gethostbyname(serverIp);
if(ent==NULL)
{
cout<<"获取主机名出错"<<WSAGetLastError()<<endl;
}
else
{
nAddr=*((u_long *)ent->h_addr_list[0]);
}
}
if(nAddr==INADDR_NONE)
{
cout<<"解析主机地址失败"<<endl;
}
return nAddr;
}
SOCKET Client::ConnectServer(u_long serverIp,int port) //连接服务器
{
sd=socket(AF_INET,SOCK_STREAM,0);
if(sd==INVALID_SOCKET)
{
cout<<"床架套接字失败"<<endl;
return INVALID_SOCKET;
}
sockaddr_in saServer;
saServer.sin_family=AF_INET;
saServer.sin_addr.S_un.S_addr=serverIp;
saServer.sin_port=htons(port);
if(connect(sd,(sockaddr*)&saServer,sizeof(sockaddr_in))==SOCKET_ERROR)
{
cout<<"连接服务器失败"<<WSAGetLastError()<<endl;
closesocket(sd);
getchar();
return INVALID_SOCKET;
}
return sd;
}
bool Client::ProcessConnection(SOCKET sd) //进行通信
{
//-------------------------------------------------
//可以将下面代码看做设置系统缓冲区
int nRecvBuf=1024000;//设置为1000K
setsockopt(sd,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
//发送缓冲区
int nSendBuf=1024000;//设置为1000K
setsockopt(sd,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
//---------------------------------------------------------
while(true)
{
cout<<"1 UPLOAD file to Server"<<endl;
cout<<"2 DOWNLOAD file from Server"<<endl;
cout<<"3 Quit"<<endl;
int n;
cin.clear();
cin.sync();
cin>>n;
switch(n)
{
case 1:
{
//向服务器发送传送文件消息
Message::MsgSendFile msgSendFile;
if(send(sd,(char *)&msgSendFile,sizeof(Message::MsgSendFile),0)==SOCKET_ERROR)
{
cout<<"发送消息失败"<<endl;
return false;
}
Sleep(10); //睡眠10ms保证对方将发送的消息取走
char filePath[MAX_FILE_NAME_LENGTH];
cout<<"Enter Full path like F:\\a\\b.jpg"<<endl;
FILE* pf = NULL;
while(1)
{
cin.clear();
fflush(stdin);
cin>>filePath;
pf = fopen(filePath,"r");
if(!pf)
{
cout << "the path is invalid, enter a new path again!" << endl;
pf = NULL;
filePath[0] = '\0';
}else{
fclose(pf);
break;
}
}
printf("%s\n",filePath);
char fileDrive[_MAX_DRIVE];
char fileDir[_MAX_DIR];
char fileName[_MAX_FNAME];
char fileExt[_MAX_EXT];
_splitpath(filePath,fileDrive,fileDir,fileName,fileExt); //将文件路径解析
Message::MsgFileName msgFileName;
strcat(fileName,fileExt);//abc.jpg
strcpy(msgFileName.fileName, fileName);
if(send(sd,(char *)&msgFileName,sizeof(Message::MsgFileName),0)==SOCKET_ERROR) //发送文件名
{
cout<<"发送文件名出错"<<WSAGetLastError()<<endl;
}
Sleep(10);
if(!SendFileLength(sd,filePath)) //发送文件长度
{
cout<<"发送文件长度出错"<<endl;
return false;
}
Sleep(10);
if(!SendFile(sd,filePath)) //发送文件
{
cout<<"发送文件出错"<<endl;
return false;
}
}
break;
case 2:
{
Message::MsgDownLoadFile msgDownLoadFile;
if(send(sd,(char *)&msgDownLoadFile,sizeof(Message::MsgDownLoadFile),0)==SOCKET_ERROR)
{
cout<<"发送下载文件消息失败"<<WSAGetLastError()<<endl;
return false;
}
if(!RecvCatalogInfo(sd))
{
return false;
}
if(!SendDownLoadFileName(sd))
{
return false;
}
if(!ReceiveFileLength(sd))
{
return false;
}
if(!ReceiveFileName(sd))
{
return false;
}
if(!ReceiveFile(sd))
{
return false;
}
}
break;
case 3:
exit(0);
break;
default:
cout<<"你输入的不符合要求,重新输入"<<endl;
}
}
return true;
}
bool Client::SendFileLength(SOCKET sd,char *filePath)
{
FILE *pFile = fopen(filePath,"r+");
if(!pFile)
{
cout<<"打开文件失败"<<WSAGetLastError() << "尝试创建新文件"<<endl;
//return false;
pFile = fopen(filePath,"wb");
if(!pFile)
{
return false;
}
}
fseek(pFile,0,SEEK_END);
nFileLength=_ftelli64(pFile);
Message::MsgFileLength msgFileLength;
msgFileLength.fileLength=nFileLength;
//if(pFile)
fclose(pFile);
if(send(sd,(char *)&msgFileLength,sizeof(Message::MsgFileLength),0)==SOCKET_ERROR)
{
return false;
}
return true;
}
bool Client::SendFile(SOCKET sd,char *filePath) //发送文件(绝对路径)
{
cout<<"进入到发送文件内容"<<endl;
Message::MsgFile msgFile;
if(send(sd,(char *)&msgFile,sizeof(Message::MsgFile),0)==SOCKET_ERROR)
{
cout<<"发送文件消息出错"<<WSAGetLastError()<<endl;
return false;
}
Sleep(10);
FILE *pFile;
pFile=fopen(filePath,"r+b");
fseek(pFile,0,SEEK_SET); //定位到文件首位置
_int64 i=0;
char buff[MAX_PACK_SIZE];
//int nSend=0;
while(i<nFileLength)
{
int nSize;
if(MAX_PACK_SIZE > nFileLength - i)
nSize = nFileLength - i;
else
nSize = MAX_PACK_SIZE - 1;
fread(buff,sizeof(char),nSize/*(int)(nFileLength-nSend)*/,pFile);
int nSend = send(sd,buff,nSize /*nFileLength-nSend*/,0);
if(nSend==SOCKET_ERROR)
{
cout<<"发送失败"<<endl;
return false;
}
i += nSend;
//fseek(pFile,i,SEEK_SET);
fseek(pFile,(long)i,SEEK_SET); //定位到实际已发送到的位置
memset(buff,0,sizeof(char)*MAX_PACK_SIZE); //将buff清空
}
fclose(pFile);
return true;
}
bool Client::RecvCatalogInfo(SOCKET sd) //接收目录信息
{
int flag=1; //接收目录信息成功标志
char buff[MAX_PACK_SIZE];
Message::MsgHead *msgHead;
while(true)
{
if(recv(sd,buff,MAX_PACK_SIZE,0)==SOCKET_ERROR)
{
cout<<"接收目录信息失败"<<WSAGetLastError()<<endl;
flag=0;
break;
}
msgHead=(Message::MsgHead *)buff;
if(msgHead->msgId==MSG_COMPLETE) //判断消息是否是标准消息
{
cout<<"目录信息发送完成"<<endl;
break;
}
else
{
cout<<buff<<endl; //发送来的是目录信息,即文件名
}
}
if(flag==0)
{
return false;
}
return true;
}
bool Client::SendDownLoadFileName(SOCKET sd) //发送下载的文件名
{
cout<<"请输入你要下载的文件名"<<endl;
char fileName[_MAX_FNAME+_MAX_EXT];
cin>>fileName;
Message::MsgFileName msgFileName;
strcpy(msgFileName.fileName,fileName);
if(send(sd,(char *)&msgFileName,MAX_PACK_SIZE,0)==SOCKET_ERROR)
{
cout<<"发送下载文件名出错"<<WSAGetLastError()<<endl;
return false;
}
return true;
}
bool Client::ReceiveFileLength(SOCKET sd) //接收下载的文件长度
{
char buff[MAX_PACK_SIZE];
Message::MsgFileLength *msgFileLength;
if(recv(sd,buff,MAX_PACK_SIZE,0)==SOCKET_ERROR)
{
cout<<"接收文件长度失败"<<WSAGetLastError()<<endl;
return false;
}
msgFileLength=(Message::MsgFileLength *)buff;
nFileLength=msgFileLength->fileLength;
cout<<"接收到文件长度"<<nFileLength<<endl;
return true;
}
bool Client::ReceiveFileName(SOCKET sd) //接收下载的文件名
{
char buff[MAX_PACK_SIZE];
memset(fileName,0,sizeof(char)*(_MAX_FNAME+_MAX_EXT));
Message::MsgFileName *msgFileName;
if(recv(sd,buff,MAX_PACK_SIZE,0)==SOCKET_ERROR)
{
cout<<"接收文件名出错"<<endl;
return false;
}
msgFileName=(Message::MsgFileName *)buff;
strcpy(fileName,msgFileName->fileName);
cout<<"接收到文件名"<<fileName<<endl;
return true;
}
bool Client::ReceiveFile(SOCKET sd) //接收文件内容
{
char buff[MAX_PACK_SIZE];
/*
FILE *pFile;
pFile=fopen(fileName,"a+b");
*/
char* strPath = "D:\\图片\\";
char* strExt = ".jpg";
strcat(strPath,strcat(fileName,strExt));
FILE* pFile = fopen(strPath,"a+b");
_int64 i=0;
while(i+1<nFileLength)
{
int nRecv=recv(sd,buff,MAX_PACK_SIZE,0);
if(nRecv==SOCKET_ERROR)
{
return false;
}
fwrite(buff,sizeof(char),nRecv,pFile);
i+=nRecv;
memset(buff,0,sizeof(char)*MAX_PACK_SIZE);
}
fclose(pFile);
return true;
}
void Client::CloseSocket() //关闭套接字
{
closesocket(sd);
WSACleanup();
}
TCHAR* Client::GetCurDir(){
TCHAR szFilePath[MAX_PATH + 1]={0};
GetModuleFileName(NULL, szFilePath, MAX_PATH);
(_tcsrchr(szFilePath, _T('\\')))[0] = 0; // 删除文件名,只获得路径字串
return szFilePath; // 例如str_url==e:\program\Debug
}
client.h
//client.h
#pragma once
#include<iostream>
#include<fstream>
#include<vector>
#include<WinSock2.h>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;
#define SERVER_IP "这里写服务器端的IP"
#define PORT 10001
class Client
{
public:
_int64 nFileLength;
char fileName[_MAX_FNAME+_MAX_EXT];
SOCKET sd;
bool InitSock(); //初始化winsock
u_long ResolveAdress(char *serverIp); //解析服务器地址
SOCKET ConnectServer(u_long serverIp,int port);//连接服务器
bool ProcessConnection(SOCKET sd); //客户端服务器交互
void CloseSocket(); //释放套接字
bool SendFileLength(SOCKET sd,char *filePath); //发送文件长度
bool SendFile(SOCKET sd,char *filePath); //发送文件
bool RecvCatalogInfo(SOCKET sd); //接收目录信息
bool SendDownLoadFileName(SOCKET sd); //发送要下载的文件名
bool ReceiveFileLength(SOCKET sd); //接收文件长度
bool ReceiveFileName(SOCKET sd); //接收文件名
bool ReceiveFile(SOCKET sd); //接收文件
TCHAR* GetCurDir();
//void DoWork(); //主体函数
};
客户端输入微信安装包的路径
服务器后来显示如下,大小也对应
root@ley:~/myStudy/server$ ls
????? a.out getpwd.c ser.c server_version WeChatSetup.exe
abc.txt def.txt message.h ser.cpp sss.cpp
说下遇到的几个新手坑:
1 recv会报错.有人说客户端请求数据为0字节时,说明已经关闭连接了,但是这时候如果在 if(recv_data_size == 0)里关闭socket,recv会报错
2 地址重用很有用
3 send和recv顺序对应,如果while里先发送fileName ,然后fileLength, 接收时如果先接收长度再名字,就会报错.
4 WSAGetLastError()很好用