Linux c/c++ 网络编程:TCP客户端/服务器实现

服务器 专栏收录该内容
1 篇文章 0 订阅

前言

前阵子我下载了基于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()很好用

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值