今年大二下OS lab从HTTP转变为FTP,要求实现客户端与服务端。参考文档为RFC 959,语言不限(LJJ老师啥都没说),做法不限(兴许不要import ftp就行。。。),这里细数一下客户端编写过程中踩过的坑。由于还没有提交作业,就不贴ugly的代码了。。。
样例FTP客户端基于C++及Poco编写,利用Poco的StreamSocket或者DialogSocket与远端FTP服务器交互。FTP是一个有状态协议,但对于状态,大部分的工作只要服务器记住就行。客户端就可以乱来了。
FTP与远端包含两个连接,ctrl-connection 和 data-connection。大部分的命令通过ctrl走response,小部分(LIST,RETR,等数据交互)通过data走。对于一个用户一个会话,所有工作几乎都是阻塞的(ABOR命令除外)。因此,若实现minimum implementation,只要同步的模型就可以完成。ctrl-connection和data-connection由同一个线程发起,天然地保证N,N+1端口与远端交互。
一个基本的控制交互过程是,本地生成socket,向远端建立连接,利用socket进行实时通信。所有的交互都是明文交互,因此可以读取、解析回复是什么东西。至于各个回复,RFC959说得就很清楚了。
int Session::pasv() {
ob buf;
string format = "PASV\r\n";
RegularExpression pasv("(\\d{1,3}),(\\d{1,3})\\)");
ctrl_cnn.sendBytes(format.c_str(), format.length());
ctrl_cnn.receiveBytes(buf, buf.length());
reply rp = replyParser(buf.read());
CHECK(rp, StatusPassiveMode);
vector<string> filter;
pasv.split(rp.des, filter);
desDataPort = (stoi(filter[1]) << 8) | stoi(filter[2]);
data_cnn.connect(Net::SocketAddress(addr.host(), desDataPort));
return rp.code;
}
在这过程中,有一些让人卡壳的事情,总是导致it works, why? 这里例举了一下。
CMAKE找不到库
我以及一些同学的项目中使用了CMAKE,所谓makefile for makefile。。。然鹅引用第三方库,是需要通过find_library
和target_link_library
才能找到相应的东西。CMAKE会给makefile添加特定的链接参数,这样才可以找到我们使用的库文件。
单句的命令如下:
clang++ -g file.cpp -o -file -lPocoNet -lPocoFoundation -lm
如果没有添加链接参数,0ops,头文件找不着了。
最后CMakeList.txt大概长这样:
cmake_minimum_required(VERSION 3.0.0)
project(final_lab_v2 VERSION 0.1.0)
set(CMAKE_CXX_STANDARD 17)
include(CTest)
enable_testing()
add_executable(final_lab_v2 main.cpp
FrontEnd.cpp
Connector.cpp
Session.cpp
OnceBuffer.cpp
Buffer.cpp)
find_library(POCO_NET PocoNet)
find_library(POCO_FD PocoFoundation)
target_link_libraries(final_lab_v2 ${POCO_NET} ${POCO_FD})
add_definitions(-I/usr/local/opt/openssl/include)
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
socket数据传输的完整性问题
这里涉及的,就包括什么时间传输完,什么时间接收完,以及粘包问题。在socket编程中,我们底层调用的函数为recv和send,而这两个函数可以设置为阻塞/非阻塞,利用多线程技术可以实现同步/异步。使用过程中发现,每次send一句命令,读的时候总能完整地读到相应的response。It works,why?
这里就要涉及阻塞、非阻塞的行为:
阻塞情况下:
在阻塞条件下,read/recv/msgrcv的行为:
1. 如果没有发现数据在网络缓冲中会一直等待,
2. 当发现有数据的时候会把数据读到用户指定的缓冲区,但是如果这个时候读到的数据量比较少,比参数中指定的长度要小,read 并不会一直等待下去,而是立刻返回。
read 的原则:是数据在不超过指定的长度的时候有多少读多少,没有数据就会一直等待。
所以一般情况下:我们读取数据都需要采用循环读的方式读取数据,因为一次read 完毕不能保证读到我们需要长度的数据,read 完一次需要判断读到的数据长度再决定是否还需要再次读取。
非阻塞情况下:
在非阻塞的情况下,read 的行为:
1. 如果发现没有数据就直接返回,
2. 如果发现有数据那么也是采用有多少读多少的进行处理.
所以:read 完一次需要判断读到的数据长度再决定是否还需要再次读取。
对于读而言: 阻塞和非阻塞的区别在于没有数据到达的时候是否立刻返回.
recv 中有一个MSG_WAITALL 的参数:
recv(sockfd, buff, buff_size, MSG_WAITALL),
在正常情况下recv 是会等待直到读取到buff_size 长度的数据,但是这里的WAITALL 也只是尽量读全,在有中断的情况下recv 还是可能会被打断,造成没有读完指定的buff_size的长度。
所以即使是采用recv + WAITALL 参数还是要考虑是否需要循环读取的问题,在实验中对于多数情况下recv (使用了MSG_WAITALL)还是可以读完buff_size,所以相应的性能会比直接read 进行循环读要好一些。
So, 阻塞状态下,connect,send一句,receive一句,完整的,it works。正确性由Linux内核保证。参考书目:Unix网络编程。
还有一个就是粘包问题。粘包问题在golang TCP编程中是一个典型的问题。比如你发了RETR命令,服务端首先会发一个150的回复,在data传输完成之后,会发一个226的回复。如果你没有正确的时间点读到,两条回复就会粘在一起。这时候你就尴尬了,如何parse也是个问题。所以,阻塞状态下,send一个RETR,读150的response,然后再开始data_connection传送数据,然后再ctrl_cnn.recv,这样就可以保证正确的时序。粘包问题在FTP中基本就遇不到了。
一些琐碎问题
- 每次发送/接收完数据,使用完毕后,buffer都要清空,因为上一次的数据会粘在\0后面。一个字符串读完了,但\0后面可能会保留上一次的东西,在数据写/ctrl parsing的时候,就容易出错。可以分装一个OnceBuffer。用memset去清空就行了。
- 发送/接收文件必须保证读完,另外写socket/文件的时候,不要固定buffer,要根据fread/receiveBytes的返回值来确定到底写多少。比如,固定buffer长度可能导致文件末尾多出了很多无用的0的问题。
#pragma once
保证了不会重复包含。
- 多行回复(HELP)咱就不做了,反正也不会给用户裸命令(逃
- 我们客户端是connection,用TCPserver是不行的(这是fei hua。。)
- unique_ptr大幅度降低内存泄漏问题
- 用python写会更加快,会更加没有平台相关性,甚至可以用来写GUI。
坑都是一起踩的坑,这里感谢沉迷MC的黄板桥同学,沉迷WOW的向日葵同学,和吃麦当劳的ZLT同学hhhhhh