网络编程基础 一个简单的网络通讯模型进阶(封装socket客户端/服务端,多进程的服务端,实现文件传输功能)

一.关于先前基础

请参见上一篇文章:

http://t.csdnimg.cn/T6sxr

二.封装socket客户端


#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
int main(int args,char *argv[])
{
if(args!=3){cout<<"Using ./test1 服务端IP 服务端端口\n";return -1;}
//第一步,创建客户端的socket
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){//如果创建失败返回-1
        perror("socket");
        return -1;
    }
    //第二步,向服务端发出连接请求
    struct sockaddr_in servaddr;//创建用于存放IP 端口和协议的结构体
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family=AF_INET;//填写协议族
    servaddr.sin_port=htons(atoi(argv[2]));//填写指定端口
    struct hostent *h;//创建hostent结构体h
    if((h=gethostbyname(argv[1]))==nullptr){//gethosbyname函数获取IP为大端序
        cout<<"grthosbyname/failed\n";
        close(sockfd);
        return -1;
    };
    memcpy(&servaddr.sin_addr,h->h_addr,sizeof(h->h_length));//将IP大端序拷贝到aervaddr结构体中
    if(connect(sockfd,(struct sockaddr*)&servaddr,sizeof(sockaddr))==-1){
        perror("connect");
        close(sockfd);
        return -1;
    }
    char buffer[1024];
    for (int ii=0;ii<10;ii++) // 循环 10 次,将与服务端进行三次通讯。
{
int iret;
memset(buffer,0,sizeof(buffer));
sprintf(buffer,"测试开始,收到请回复..."); // 生成请求报文内容。
// 向服务端发送请求报文。
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
memset(buffer,0,sizeof(buffer));
// 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0)
{
cout << "iret=" << iret << endl; break;
}
cout << "接收:" << buffer << endl;
sleep(1);
}
// 第 4 步:关闭 socket,释放资源。
close(sockfd);
}

代码用到的数据结构和函数比较复杂,为了方便使用,将他封装成类ctcplient使用:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
class ctcpclient // TCP 通讯的客户端类。
{
private:
int m_clientfd; // 客户端的 socket,-1 表示未连接或连接已断开;>=0 表示有效的socket。
string m_ip; // 服务端的 IP/域名。
unsigned short m_port; // 通讯端口。
public:
ctcpclient():m_clientfd(-1) {}
// 向服务端发起连接请求,成功返回 true,失败返回 false。
bool connect(const string &in_ip,const unsigned short in_port){
if (m_clientfd!=-1) return false; // 如果 socket 已连接,直接返回失败。
m_ip=in_ip; m_port=in_port; // 把服务端的 IP 和端口保存到成员变量中。
// 第 1 步:创建客户端的 socket。
if ( (m_clientfd = socket(AF_INET,SOCK_STREAM,0))==-1) return false;
// 第 2 步:向服务器发起连接请求。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口。
struct hostent* h; // 用于存放服务端 IP 地址(大端序)的结构体的指针。
if ((h=gethostbyname(m_ip.c_str()))==nullptr ) // 把域名/主机名/字符串格式的 IP 转换成结构体。
{
::close(m_clientfd); m_clientfd=-1; return false;
}
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // ③指定服务端的 IP(大端序)。
// 向服务端发起连接清求。
if (::connect(m_clientfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
::close(m_clientfd); m_clientfd=-1; return false;
}
return true;
}
// 向服务端发送报文,成功返回 true,失败返回 false。
bool send(const string &buffer){ // buffer 不要用 const char 
if (m_clientfd==-1) return false; // 如果 socket 的状态是未连接,直接返回失败。
if ((::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
// 接收服务端的报文,成功返回 true,失败返回 false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen){ // 如果直接操作 string 对象的内存,必须保证:1)不能越界;2)操作后手动设置数据的大小。
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为 maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作 buffer 的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置 buffer 的实际大小。
return true;
}
// 断开与服务端的连接。
bool close(){
if (m_clientfd==-1) return false; // 如果 socket 的状态是未连接,直接返回失败。
::close(m_clientfd);
m_clientfd=-1;
return true;
}
~ctcpclient(){ close(); }
};
int main(int argc,char *argv[])
{
if (argc!=3){
cout << "Using:./demo7 服务端的 IP 服务端的端口\n";return -1;
}
ctcpclient tcpclient;
if (tcpclient.connect(argv[1],atoi(argv[2]))==false){// 向服务端发起连接请求。
perror("connect()"); return -1;
}
// 第 3 步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。
string buffer;
for (int ii=0;ii<10;ii++){// 循环10次,将与服务端进行三次通讯。
buffer="Test "+to_string(ii+1)+"  start...";
// 向服务端发送请求报文。
if (tcpclient.send(buffer)==false){
perror("send"); break;
}
cout << "发送:" << buffer << endl;
// 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
if (tcpclient.recv(buffer,1024)==false){
perror("recv()"); break;
}
cout << "接收:" << buffer << endl;
sleep(1);
}
}

三.封装socket服务端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
int main(int argc,char *argv[])
{
if(argc!=2){
    cout<<"Using ./test2 端口"<

将他封装成类ctcpserver使用:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
class ctcpserver // TCP 通讯的服务端类。
{
private:
int m_listenfd; // 监听的 socket,-1 表示未初始化。
int m_clientfd; // 客户端连上来的 socket,-1 表示客户端未连接。
string m_clientip; // 客户端字符串格式的 IP。
unsigned short m_port; // 服务端用于通讯的端口。
public:
ctcpserver():m_listenfd(-1),m_clientfd(-1) {}
// 初始化服务端用于监听的 socket。
bool initserver(const unsigned short in_port){
// 第 1 步:创建服务端的 socket。
if ( (m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) return false;
m_port=in_port;
// 第 2 步:把服务端用于通信的 IP 和端口绑定到 socket 上。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port=htons(m_port); // ②指定服务端的通信端口。
servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个 IP,全部的 IP 都可以用于通讯。
// 绑定服务端的 IP 和端口(为 socket 分配 IP 和端口)。
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1){
close(m_listenfd); m_listenfd=-1; return false;
}
// 第 3 步:把 socket 设置为可连接(监听)的状态。
if (listen(m_listenfd,5) == -1 ){
close(m_listenfd); m_listenfd=-1; return false;
}
return true;
}
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
bool accept(){
struct sockaddr_in caddr; // 客户端的地址信息。
socklen_t addrlen=sizeof(caddr); // struct sockaddr_in 的大小。
if ((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1) return
false;
m_clientip=inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串。
return true;
}
// 获取客户端的 IP(字符串格式)。
const string & clientip() const{
return m_clientip;
}
// 向对端发送报文,成功返回 true,失败返回 false。
bool send(const string &buffer){
if (m_clientfd==-1) return false;
if ( (::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
// 接收对端的报文,成功返回 true,失败返回 false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen){
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为 maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作 buffer 的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置 buffer 的实际大小。
return true;
}
// 关闭监听的 socket。
bool closelisten(){
if (m_listenfd==-1) return false;
::close(m_listenfd);
m_listenfd=-1;
return true;
}
// 关闭客户端连上来的 socket。
bool closeclient(){
if (m_clientfd==-1) return false;
::close(m_clientfd);
m_clientfd=-1;
return true;
}
~ctcpserver() { closelisten(); closeclient(); }
};
int main(int argc,char *argv[])
{
if (argc!=2){
cout << "Using:./demo8 通讯端口\nExample:./demo8 5005\n\n"; // 端口大于 1024,不与其它的重复。
cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通 5005 端口。\n";
return -1;
}
ctcpserver tcpserver;
if (tcpserver.initserver(atoi(argv[1]))==false){// 初始化服务端用于监听的 socket。
perror("initserver()"); return -1;
}
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
if (tcpserver.accept()==false){
perror("accept()"); return -1;
}
cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";
string buffer;
while (true){
// 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待。
if (tcpserver.recv(buffer,1024)==false){
perror("recv()"); break;
}
cout << "接收:" << buffer << endl;
buffer="ok";
if (tcpserver.send(buffer)==false){// 向对端发送报文。
perror("send"); break;
}
cout << "发送:" << buffer << endl;
}
}

四.多进程的服务端

一对一的客户/服务端在实际应用中几乎没有,所以下一步是把服务端升级为多进程的,就像生活中可以用不同的设备、途径去访问一个后台程序。在精简的模型基础上将不断受理客户端连接的部分作为父进程,与不同IP的客户端通讯的部分作为子进程,也就是一个服务端受理多个客户端,则客户端的模型不用改动。父进程和子进程也要接受自定义的信号命令,满足调度需求的同时也方便解决僵尸进程,释放资源的问题。

关于多进程与信号,linux信号的问题都在这两篇文章:

多进程与信号

http://t.csdnimg.cn/PoCEg

linux信号(C++):

http://t.csdnimg.cn/ODyRh

详细内容在代码里了,个别陌生的函数也可以在文章中或直接查找,系统内man命令手册都可以解决。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

class ctcpserver // TCP 通讯的服务端类。
{
private:
int m_listenfd; // 监听的 socket,-1 表示未初始化。
int m_clientfd; // 客户端连上来的 socket,-1 表示客户端未连接。
string m_clientip; // 客户端字符串格式的 IP。
unsigned short m_port; // 服务端用于通讯的端口。
public:
ctcpserver():m_listenfd(-1),m_clientfd(-1) {}
// 初始化服务端用于监听的 socket。
bool initserver(const unsigned short in_port){
// 第 1 步:创建服务端的 socket。
if ( (m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) return false;
m_port=in_port;
// 第 2 步:把服务端用于通信的 IP 和端口绑定到 socket 上。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port=htons(m_port); // ②指定服务端的通信端口。
servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个 IP,全部的 IP 都可以用于通讯。
// 绑定服务端的 IP 和端口(为 socket 分配 IP 和端口)。
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1){
close(m_listenfd); m_listenfd=-1; return false;
}
// 第 3 步:把 socket 设置为可连接(监听)的状态。
if (listen(m_listenfd,5) == -1 ){
close(m_listenfd); m_listenfd=-1; return false;
}
return true;
}
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
bool accept(){
struct sockaddr_in caddr; // 客户端的地址信息。
socklen_t addrlen=sizeof(caddr); // struct sockaddr_in 的大小。
if ((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1) return
false;
m_clientip=inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串。
return true;
}
// 获取客户端的 IP(字符串格式)。
const string & clientip() const{
return m_clientip;
}
// 向对端发送报文,成功返回 true,失败返回 false。
bool send(const string &buffer){
if (m_clientfd==-1) return false;
if ( (::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
// 接收对端的报文,成功返回 true,失败返回 false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen){
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为 maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作 buffer 的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置 buffer 的实际大小。
return true;
}
// 关闭监听的 socket。
bool closelisten(){
if (m_listenfd==-1) return false;
::close(m_listenfd);
m_listenfd=-1;
return true;
}
// 关闭客户端连上来的 socket。
bool closeclient(){
if (m_clientfd==-1) return false;
::close(m_clientfd);
m_clientfd=-1;
return true;
}
~ctcpserver() { closelisten(); closeclient(); }
};

ctcpserver tcpserver;//信号处理函数只能访问全局变量

void FathEXIT(int sig); // 父进程的信号处理函数。
void ChldEXIT(int sig); // 子进程的信号处理函数。

int main(int argc,char *argv[])
{
if (argc!=2){
cout << "Using:./demo8 通讯端口\nExample:./demo8 5005\n\n"; // 端口大于 1024,不与其它的重复。
cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通 5005 端口。\n";
return -1;
}

//忽略全部信号不被打扰
for(int ii{};ii<64;ii++)signal(ii,SIG_IGN);
//设置信号在shell状态下可用"kill 进程号"和"Ctrl c"终止某些进程
//但请不要用 "kill -9 +进程号" 强行终止
signal(SIGTERM,FathEXIT); signal(SIGINT,FathEXIT); // SIGTERM 15 SIGINT 2

if (tcpserver.initserver(atoi(argv[1]))==false){// 初始化服务端用于监听的 socket。
perror("initserver()"); return -1;
}


while(true){
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
if (tcpserver.accept()==false){
perror("accept()"); return -1;
}
cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";
int pid=fork();
if(pid==-1){perror("fork()");return -1;}//系统资源不足
if(pid>0){tcpserver.closeclient();continue;}//用不到clientfd关闭客户端socket,
//父进程回到循环开始的位置,继续受理客户端连接
//关闭监听,子进程用不着监听
tcpserver.closelisten();
//子进程负责和客户端通讯
signal(SIGTERM,ChldEXIT); // 子进程的退出函数与父进程不一样。
signal(SIGINT,SIG_IGN); // 子进程不需要捕获 SIGINT 信号。

string buffer;
while (true){
// 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待。
if (tcpserver.recv(buffer,1024)==false){
perror("recv()"); break;
}
cout << "接收:" << buffer << endl;
buffer="ok";
if (tcpserver.send(buffer)==false){// 向对端发送报文。
perror("send"); break;
}
cout << "发送:" << buffer << endl;
}
return 0;//子进程一定要退出,否则又会回到accept()的地方
}
}
void FathEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "父进程退出,sig=" << sig << endl;
kill(0,SIGTERM); // 向全部的子进程发送 15 的信号,通知它们退出。
// 在这里增加释放资源的代码(全局的资源)。
//父进程应当释放socket listenfd
tcpserver.closelisten();
exit(0);
}
// 子进程的信号处理函数。
void ChldEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "子进程" << getpid() << "退出,sig=" << sig << endl;
// 在这里增加释放资源的代码(只释放子进程的资源)。
//父进程应当释放socket clientfd
tcpserver.closeclient();
exit(0);
}

开多几个客户端的运行结果:

网络编程基础 一个简单的网络通讯模型进阶(封装socket客户端/服务端,多进程的服务端,实现文件传输功能)_第1张图片

五.实现文件传输功能

实现文件传输功能,支持客户端向服务端发送文本文件或二进制文件

1.关于客户端:

客户端需要增加一个文件属性结构体(文件名,文件大小)和一个发送文件属性的ctcpclient成员函数(重载send()),一个发送文件的ctcpclient成员函数(sendfile()),重点在于发送文件的成员函数tcplient::sendfile(),具体代码里有详细流程方法注释:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
class ctcpclient // TCP 通讯的客户端类。
{
private:
int m_clientfd; // 客户端的 socket,-1 表示未连接或连接已断开;>=0 表示有效的socket。
string m_ip; // 服务端的 IP/域名。
unsigned short m_port; // 通讯端口。
public:
ctcpclient():m_clientfd(-1) {}
// 向服务端发起连接请求,成功返回 true,失败返回 false。
bool connect(const string &in_ip,const unsigned short in_port){
if (m_clientfd!=-1) return false; // 如果 socket 已连接,直接返回失败。
m_ip=in_ip; m_port=in_port; // 把服务端的 IP 和端口保存到成员变量中。
// 第 1 步:创建客户端的 socket。
if ( (m_clientfd = socket(AF_INET,SOCK_STREAM,0))==-1) return false;
// 第 2 步:向服务器发起连接请求。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口。
struct hostent* h; // 用于存放服务端 IP 地址(大端序)的结构体的指针。
if ((h=gethostbyname(m_ip.c_str()))==nullptr ) // 把域名/主机名/字符串格式的 IP 转换成结构体。
{
::close(m_clientfd); m_clientfd=-1; return false;
}
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // ③指定服务端的 IP(大端序)。
// 向服务端发起连接清求。
if (::connect(m_clientfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
::close(m_clientfd); m_clientfd=-1; return false;
}
return true;
}

// 向服务端发送报文(字符串),成功返回 true,失败返回 false。
bool send(const string &buffer){ // buffer 不要用 const char 
if (m_clientfd==-1) return false; // 如果 socket 的状态是未连接,直接返回失败。
if ((::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}

// 向服务端发送报文(二进制文件),成功返回 true,失败返回 false。
bool send(void *buffer,const size_t size){ // buffer 不要用 const char 
if (m_clientfd==-1) return false; // 如果 socket 的状态是未连接,直接返回失败。
if ((::send(m_clientfd,buffer,size,0))<=0) return false;
return true;
}

//向服务端发送文件内容
bool sendfile(const string& filename,const size_t filesize){
//以二进制的方法打开文件
ifstream fin(filename,ios::binary);
if(fin.is_open()==false){cout<<"打开文件:"<4096)onread=4096;//如果剩余数据字节数大于4096,则这次循环打算读取的数据字节数为7
else onread=filesize-totalbytes;//否则就读取剩余数据的字节数

fin.read(buffer,onread);//读取数据并存入buffer

if(send(buffer,onread)==false)//向服务端发送数据return false;
totalbytes+=onread;//更新已读数据量
if(totalbytes==filesize)break;//如果全部数据读取完break
}
return true;
}

// 接收服务端的报文,成功返回 true,失败返回 false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen){ // 如果直接操作 string 对象的内存,必须保证:1)不能越界;2)操作后手动设置数据的大小。
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为 maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作 buffer 的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置 buffer 的实际大小。
return true;
}
// 断开与服务端的连接。
bool close(){
if (m_clientfd==-1) return false; // 如果 socket 的状态是未连接,直接返回失败。
::close(m_clientfd);
m_clientfd=-1;
return true;
}
~ctcpclient(){ close(); }
};
int main(int argc,char *argv[])
{
if (argc!=5){
cout << "Using:./demo7 服务端的IP 服务端的端口 文件名 文件大小\n";return -1;
}
ctcpclient tcpclient;
if (tcpclient.connect(argv[1],atoi(argv[2]))==false){// 向服务端发起连接请求。
perror("connect()"); return -1;
}
//发送文件流程
//1)把待传输文件名和文件大小通知服务端
struct st_fileinfo{//文件结构体
char filename[256];
int filesize;
}fileinfo;
memset(&fileinfo,0,sizeof(fileinfo));//初始化结构体
strcpy(fileinfo.filename,argv[3]);
fileinfo.filesize=atoi(argv[4]);
if(tcpclient.send(&fileinfo,sizeof(fileinfo))==false){//发送文件
perror("send()");return -1;
}
cout<<"发送文件:"<

2.关于服务端:

服务端也同样需要一个文件属性结构体(文件名,文件大小)来存放来自客户端发送的文件的文件信息,以及接收文件属性的ctcpserver成员函数(重载recv()),一个接收文件的ctcpserver成员函数(recvfile()),重点在于接收文件的成员函数tcpserver::recvfile(),具体代码里有详细流程方法注释:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

class ctcpserver // TCP 通讯的服务端类。
{
private:
int m_listenfd; // 监听的 socket,-1 表示未初始化。
int m_clientfd; // 客户端连上来的 socket,-1 表示客户端未连接。
string m_clientip; // 客户端字符串格式的 IP。
unsigned short m_port; // 服务端用于通讯的端口。
public:
ctcpserver():m_listenfd(-1),m_clientfd(-1) {}
// 初始化服务端用于监听的 socket。
bool initserver(const unsigned short in_port){
// 第 1 步:创建服务端的 socket。
if ( (m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) return false;
m_port=in_port;
// 第 2 步:把服务端用于通信的 IP 和端口绑定到 socket 上。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port=htons(m_port); // ②指定服务端的通信端口。
servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个 IP,全部的 IP 都可以用于通讯。
// 绑定服务端的 IP 和端口(为 socket 分配 IP 和端口)。
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1){
close(m_listenfd); m_listenfd=-1; return false;
}
// 第 3 步:把 socket 设置为可连接(监听)的状态。
if (listen(m_listenfd,5) == -1 ){
close(m_listenfd); m_listenfd=-1; return false;
}
return true;
}
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
bool accept(){
struct sockaddr_in caddr; // 客户端的地址信息。
socklen_t addrlen=sizeof(caddr); // struct sockaddr_in 的大小。
if ((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1) return
false;
m_clientip=inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串。
return true;
}
// 获取客户端的 IP(字符串格式)。
const string & clientip() const{
return m_clientip;
}
// 向对端发送报文,成功返回 true,失败返回 false。
bool send(const string &buffer){
if (m_clientfd==-1) return false;
if ( (::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
// 接收客户端的报文,成功返回 true,失败返回 false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen){
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为 maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作 buffer 的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置 buffer 的实际大小。
return true;
}

// 接收客户端的报文,成功返回 true,失败返回 false。
bool recv(void *buffer,const size_t size){
if(::recv(m_clientfd,buffer,size,0)<=0)return false;
return true;
}

bool recvfile(const string &filename,const size_t filesize){
ofstream fout(filename,ios::binary);
if(fout.is_open()==false){cout<<"打开文件失败"<4096)onread=4096;
else onread=filesize-totalbytes;

if(recv(buffer,onread)==false)return false;//接收文件数据

fout.write(buffer,onread);//将接收的文件写入
totalbytes+=onread;//更新已接收文件字节数

if(filesize==totalbytes)break;
}
return true;
}

// 关闭监听的 socket。
bool closelisten(){
if (m_listenfd==-1) return false;
::close(m_listenfd);
m_listenfd=-1;
return true;
}
// 关闭客户端连上来的 socket。
bool closeclient(){
if (m_clientfd==-1) return false;
::close(m_clientfd);
m_clientfd=-1;
return true;
}
~ctcpserver() { closelisten(); closeclient(); }
};

ctcpserver tcpserver;//信号处理函数只能访问全局变量

void FathEXIT(int sig); // 父进程的信号处理函数。
void ChldEXIT(int sig); // 子进程的信号处理函数。

int main(int argc,char *argv[])
{
if (argc!=3){
cout << "Using:./demo8 通讯端口\nExample:./test 5005 文件保存目录\n\n"; // 端口大于 1024,不与其它的重复。
cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通 5005 端口。\n";
return -1;
}

//忽略全部信号不被打扰
for(int ii{};ii<64;ii++)signal(ii,SIG_IGN);
//设置信号在shell状态下可用"kill 进程号"和"Ctrl c"终止某些进程
//但请不要用 "kill -9 +进程号" 强行终止
signal(SIGTERM,FathEXIT); signal(SIGINT,FathEXIT); // SIGTERM 15 SIGINT 2

if (tcpserver.initserver(atoi(argv[1]))==false){// 初始化服务端用于监听的 socket。
perror("initserver()"); return -1;
}


while(true){
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
if (tcpserver.accept()==false){
perror("accept()"); return -1;
}
cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";
int pid=fork();
if(pid==-1){perror("fork()");return -1;}//系统资源不足
if(pid>0){tcpserver.closeclient();continue;}//用不到clientfd关闭客户端socket,
//父进程回到循环开始的位置,继续受理客户端连接
//关闭监听,子进程用不着监听
tcpserver.closelisten();
//子进程负责和客户端通讯
signal(SIGTERM,ChldEXIT); // 子进程的退出函数与父进程不一样。
signal(SIGINT,SIG_IGN); // 子进程不需要捕获 SIGINT 信号。

//接收文件流程
//1)接收来自客户端的文件的文件名和文件大小信息
struct st_fileinfo{
char filename[256];
int filesize;
}fileinfo;
memset(&fileinfo,0,sizeof(fileinfo));
if(tcpserver.recv(&fileinfo,sizeof(fileinfo))==false){
    perror("recv()");return -1;
}
cout<<"接收文件:"<

服务端界面运行测试结果:

网络编程基础 一个简单的网络通讯模型进阶(封装socket客户端/服务端,多进程的服务端,实现文件传输功能)_第2张图片

Ctrl+c服务端退出父子进程。

这样一个简单的网络通讯演示模型基本基本就完成了,它有了最简单基础的客户端与服务端的通讯功能,在更深入的学习后会优化成功能更多更强大更合理的真正的网络通讯程序。

你可能感兴趣的:(网络,tcp/ip,网络协议,centos,c++,linux,c语言)