简介:基于TCP协议开发文件传输系统,搭建多进程网络服务程序框架,实现TCP长连接心跳机制、文件上传与下载、异步通信实现快速传输。
socket通讯
什么是Socket?
Socket(套接字),用来描述IP地址和端口,是通信链的句柄,是支持TCP/IP协议的网络通信的基本操作单元,是对网络通信过程中端点的抽象表示。
通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。Socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
插述:
TCP(Transmission Control Protocol)传输控制协议,是一种面向连接的、可靠的、基于字节流的通信协议。数据在传输前要建立连接,传输完毕后还要断开连接。客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。
TCP粘包问题
socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。
例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。
假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。
这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
测试粘包:
TCP粘包问题解决
采用自定义的报文格式:报文长度+报文内容
(*ibuflen) = 0; // 报文长度变量初始化为0。
// 先读取报文长度,4个字节。
if (Readn(sockfd,(char*)ibuflen,4) == false) return false;
(*ibuflen)=ntohl(*ibuflen); // 把报文长度由网络字节序转换为主机字节序。
// 再读取报文内容。
if (Readn(sockfd,buffer,(*ibuflen)) == false) return false;
关于TCP缓冲区
什么是tcp缓冲区?
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
缓冲区的意义
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回
,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数
,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,比如nagle算法,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据
,而不是直接从网络中读取。
I/O缓冲区特性
1、I/O缓冲区在每个TCP套接字中单独存在;
2、I/O缓冲区在创建套接字时自动生成;
3、即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4、关闭套接字将丢失输入缓冲区中的数据。
输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:
int servSock = socket(PF_INET, SOCK_STREAM, 0);
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);
运行结果:
Buffer length: 8192
封装socket常用函数
bool CTcpServer::InitServer(const unsigned int port,const int backlog)
{
// 如果服务端的socket>0,关掉它,这种处理方法没有特别的原因,不要纠结。
if (m_listenfd > 0) { close(m_listenfd); m_listenfd=-1; }
if ( (m_listenfd = socket(AF_INET,SOCK_STREAM,0))<=0) return false;
// 忽略SIGPIPE信号,防止程序异常退出。
signal(SIGPIPE,SIG_IGN);
// 打开SO_REUSEADDR选项,当服务端连接处于TIME_WAIT状态时可以再次启动服务器,
// 否则bind()可能会不成功,报:Address already in use。
//char opt = 1; unsigned int len = sizeof(opt);
int opt = 1; unsigned int len = sizeof(opt);
setsockopt(m_listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,len);
memset(&m_servaddr,0,sizeof(m_servaddr));
m_servaddr.sin_family = AF_INET;
m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
m_servaddr.sin_port = htons(port);
if (bind(m_listenfd,(struct sockaddr *)&m_servaddr,sizeof(m_servaddr)) != 0 )
{
CloseListen(); return false;
}
if (listen(m_listenfd,backlog) != 0 )
{
CloseListen(); return false;
}
return true;
}
bool CTcpServer::Accept()
{
if (m_listenfd==-1) return false;
m_socklen = sizeof(struct sockaddr_in);
if ((m_connfd=accept(m_listenfd,(struct sockaddr *)&m_clientaddr,(socklen_t*)&m_socklen)) < 0)
return false;
return true;
}
析构函数中会释放socket连接。
多进程网络服务程序框架
TCP长连接与短连接
1、使用方法不同
。长连接是client方与server方先建立连接,连接建立后不断开,然后再进行报文发送和接收。短连接是Client方与server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此方式常用于一点对多点通讯。
2、操作过程不同
。长连接的操作步骤是:建立连接、数据传输…、保持连接
、数据传输、关闭连接。短连接的操作步骤是:建立连接、数据传输、关闭连接、建立连接、数据传输、关闭连接。
3、使用时机不同
。长连接:短连接多用于操作频繁,点对点的通讯,而且长连接数不能太多的情况。每个TCP连接的建立都需要三次握手,每个TCP连接的断开要四次握手。
在HTTP/1.0中,默认使用的是短连接,也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源,如js文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。
但从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头中加入 Connection:keep-alive
心跳机制
:心跳机制是定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。
客户端心跳:以xml格式向客户端发送心跳包
// 心跳。
bool srv000()
{
char buffer[1024];
SPRINTF(buffer,sizeof(buffer),"<srvcode>0</srvcode>");
printf("发送:%s\n",buffer);
if (TcpClient.Write(buffer)==false) return false; // 向服务端发送请求报文。
memset(buffer,0,sizeof(buffer));
if (TcpClient.Read(buffer)==false) return false; // 接收服务端的回应报文。
printf("接收:%s\n",buffer);
return true;
}
多进程的服务程序
一个服务端响应客户端,服务端程序不会退出。
子进程处理业务,父进程继续accept,接受客户端的请求。
加入子进程,解决了子进程不能退出,在循环内退出后,会产生僵尸进程
。
解决僵尸进程
:信号处理函数中,加入single()函数,则父进程不需要等待。
while (true)
{
// 等待客户端的连接请求。
if (TcpServer.Accept()==false)
{
logfile.Write("TcpServer.Accept() failed.\n"); FathEXIT(-1);
}
logfile.Write("客户端(%s)已连接。\n",TcpServer.GetIP());
if (fork()>0) { TcpServer.CloseClient(); continue; } // 父进程继续回到Accept()。
// 子进程重新设置退出信号。
signal(SIGINT,ChldEXIT); signal(SIGTERM,ChldEXIT);
TcpServer.CloseListen();
// 子进程与客户端进行通讯,处理业务。
char buffer[102400];
// 与客户端通讯,接收客户端发过来的报文后,回复ok。
while (1)
{
memset(buffer,0,sizeof(buffer));
if (TcpServer.Read(buffer)==false) break; // 接收客户端的请求报文。
logfile.Write("接收:%s\n",buffer);
strcpy(buffer,"ok");
if (TcpServer.Write(buffer)==false) break; // 向客户端发送响应结果。
logfile.Write("发送:%s\n",buffer);
}
ChldEXIT(0);
扩展:僵尸进程
僵尸进程处理的三种方式
- 内核向父进程发送SIGCHLD信号,
2.wait函数
但父进程会阻塞
3.在信号处理函数中,调用wait,不需要等待
Linux一切皆文件
Linux中所有内容都是以文件的形式保存和管理,即:一切皆文件。
普通文件是文件。
目录(在win下称为文件夹)是文件。
硬件设备(键盘、硬盘、打印机)是文件。
套接字(socket)、网络通信等资源也都是文件。
启动一个 linux 进程时,程序默认打开三个 I/O 设备文件:标准输入文件 stdin,标准输出文件 stdout,标准错误输出文件 stderr
,而每打开一个文件,就有一个代表该打开文件的文件描述符(比较小的整数)。而一般情况下,stdin、stdout 和 stderr 对应的文件描述符就是 0、1、2。
查看文件描述符:
fd打开越多,则资源消耗越大,所以在工程中,总是关闭可以关闭的fd.