记录一些bug和修改以及日志
部署问题
云服务器应用防火墙设置
在服务器上部署需要先设置端口才能连接
客户端发布
- 图标
- 资源文件那添加资源,选icon,然后导入一个ico的文件即可
- vs选择release x64,然后生成即可。
内网穿透-p2p文件发送
发文件时,客户端互相connect不上,listen端无所谓,发起connect的那方马上就发现无法连接就返回了。这是因为主机在内网的缘故。
NAT与内网穿透
假设A打算发文件给B,那么服务器只将客户端B连接服务器使用的ip发给了A,对于内网用户来说,B的这个ip是路由器网关的ip,要进行NAT转换才能到内网主机,NAT就是网络地址转换的意思,因此需要进行内网穿透。
对于内网用户来说,服务器接收到一个ip和一个端口,如果是公网用户,则这个ip是主机ip,端口是主机进程使用的端口;如果是内网用户,则这个ip是网关的ip,端口是映射表中的端口,根据这个端口,网关能知道要发送给哪个主机中的哪个进程。NAT转换表的映射如下:
内网穿透(Intranet penetration
)就是通过一个公网服务器,让内网主机去连接服务器,服务器就能获取两个内网主机的ip和端口(实际上是各自的网关的ip和映射表中的端口),然后两个内网主机就可以通信了。
UDP打洞
这实际上就是内网穿透最通常的实现方式,服务器如何获取内网主机网关的ip和端口呢,总是要通过连接或者发送信息。因为TCP开销比UDP大得多,所以一般来讲都是使用UDP来实现内网穿透,所以也叫UDP打洞(UDP hole punching
)。
当然使用TCP也是可以的。
测NAT类型,用miwifi.com测试,每次打开第一次都是端口限制圆锥形,然后之后都是完全圆锥形。这是软件的问题,不管怎么样,是cone类型即可。
客户端中的问题
原来我以为已经实现了内网穿透了…但刚开始没去细想,理解还是片面了,其实并没有实现。
注意这里的端口映射是映射到一个进程的,也就是说内网穿透实际上是进程(线程)与进程(线程)之间的互相穿透。
然而客户端只有主线程和接收线程连上了服务器,发送文件和接收文件的线程并没有!这两个线程才是真正需要p2p对端发送文件的,但由于接收文件线程是临时创建的,所以需要再内网穿透,因此要让接收文件线程也去连接服务器,让发送线程获取ip端口信息才行。回顾一下先前的设计:
- 1)用户A发送sendfile请求,服务器发sendfilefrom给B
- 2)用户B发送acceptfile给服务器,并开始listen等待连接
- 3)服务器给A发一个sendfile accept + ip信息,A准备根据这个 ip 和sendfile port连接B(实际上该ip是B的网关ip,但是并没有端口映射到接收文件线程)
- A首先根据ip连接到网关
- 然后网关根据port查映射表
- 但由于B接收文件线程没有在网关中添加映射,首先网关肯定不能映射到该线程;并且网关需要查询的port也不一定就是sendfile port(因为网关添加映射是它自己添加的,port怎么样在添加前并不清楚)
因此需要B去连接服务器,一方面让网关添加映射,一方面让服务器获取网关自己添加的映射表中的端口。
简单的想法是直接让接收文件线程去TCP连接服务器,然后让其发送acceptfile命令,这里实现UDP打洞。
当用户B发送acceptfile的命令时,还是让命令主线程直接发送,接着让接收线程创建socket后直接向服务器的端口发一个信息。服务器在获取这个信息时,知道这个命令就是接收文件线程发过来的,就直接把ip和port发给发送文件线程。
更具体的实现是:
- 首先服务器新建一个listen套接字(和服务器一起初始化),这是因为另外两个套接字在连接时都会做一些后续动作(添加映射表和向epoll注册事件),这里并不需要,因为连接是一次性的。
- 然后当服务器收到acceptfile请求后,不像之前一下拼接ip就发回去了,而是调用recvfrom阻塞(如果客户端发的比较慢)等待或直接获取(如果客户端发得快)客户端sendto发送的信息(因为udp不确保正确,所以随便发就可以了),然后把ip和port拼接发给目标就可以了。
- 客户端直接创建完socket就sendto,不管服务器有没有收到,然后进行listen等待对端传输文件。
注意这里的问题不能在用户较多并且同时使用acceptfile时区分是哪个用户,这也是UDP打洞的问题所在,除非再添加其他实现。这里不搞那么复杂。
实现
定义端口号为10000,在云服务器防火墙添加UDP规则。
UDP流程如下:
主要介绍两个函数:
1 | int recvfrom(int sockfd, void * buf, size_t len, int flags, struct sockaddr * src_addr, socklen_t * addrlen); |
1 | int sendto(int sockfd, const void * buf, size_t len, int flags, const struct sockaddr * dest_addr, socklen_t addrlen); |
recvfrom相当于把accept的事情做了(保存了客户端地址端口),sendto相当于把connect的事情做了(用到了服务器的ip和port)
对于listen来说,该bind还是要bind,才能启动监听,但不用调用listen()函数。后面可以看到,创建socket的方式基本都是相同的,当创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求。
而UDP不是面向连接的,当然不用listen()了,创建出来的端口发送可接收。接收的话就要bind,仅发送就不用bind。bind的作用是,使得这个套接字的接收是从该端口接收的,发送是从该端口发送的(使得报文中的源端口是该端口)。所以一般客户端不用bind某个端口,交给系统从connect后选择,这样同样的代码可以避免bind同一个端口,否则每次都要改端口。而当需要收发端口统一时,请使用bind。
服务器端实现,初始化UDP监听端口:
1 | void init_udp_Socket(int& listenfd, const int port) |
原来的acceptfile命令的处理基本长这样,需要在sendstr前把ip和端口拿到。
1 | void acceptfile(int conn1, string sid) |
添加一个函数,放回ip和port的string。
1 | string udp_hole_punching(int listenfd) |
服务器差不多就完成了,下面是客户端的修改。客户端主要是在recvfile这个函数做修改,在函数开始前发送udp打洞信息即可,实现一个函数:
1 | bool udp_hole_punching(const char* server_ip, const int port) |
linux-windows字符集问题
linux和windows编码不一样,中文乱码
一些奇妙的bug
客户端退出问题
客户端接收线程recv阻塞退出问题
一开始很奇怪,exit后服务器关闭套接字,然后接收线程就出问题了,recv是-1然后退出一直循环。
这个问题是因为使用了
size_t recvbytes = recv(connfd, recvbuf, sizeof(recvbuf), 0);
其中size_t无法让返回值变成负数,因此判断-1失效,无法获取服务器已关闭的消息。然后发现了更怪的问题,就是服务器只close主线程的套接字,接收线程的并没有管,为什么接收线程会退出呢?
查看socket的error字段,发现是10053,即主机主动关闭了连接。思考可能是主线程退出后关了些东西使得接收线程也失败了。检查发现主线程只关了自己的套接字,(一开始没有发现)再仔细看发现执行了
WSACleanup();
使得socket都退出了,这也难怪接收线程会直接退出。然而这并不是什么不好的事情,因为本来接收线程recv阻塞也不好退出,现在刚好根据主线程退出,二者同时close掉套接字,然后服务器分别响应并close。分别响应的原因是,当客户端自己崩了的话也是二者同时close掉套接字,此时服务器也应该是分别响应的。
注意客户端调用exit后会close自己的套接字,所以服务器可以直接根据close这个信息来exit_,不需要根据exit命令来操作;这样可以把客户端正常退出和异常退出的情况合起来。
cout多线程安全问题
- cout多线程安全问题,这个在客户端里涉及。因为cout本身是流对象重载了<<函数,所以<<endl和前面的不是同个函数调用(flush同理),因此会被其他线程的cout挤掉,就导致输出混乱(主要是换行endl被挤掉了不好看)。解决方法是:
- 换行符直接写到字符串里,但cout刷新缓冲不支持,可能不能及时输出,因为\n在cout中不会刷新,刷新时机:
- 程序正常退出会刷新cout的缓冲区
- 一些输出操纵符可以帮助我们刷新,比如endl,flush,ends 代码实例: cout<<”hello”<<flush;由于重载函数,每个<<都可以被其他线程挤掉
- 将输入于输出绑定在一起,则输入会导致刷新输出的缓冲区 代码:cin.tie(&cout)
- 也可以通过unitbuf操纵符设置流的内部状态,从而清空缓冲区
- 使用printf,在printf中\n会刷新缓冲区,刷新时机
程序正常退出,输出字符带有‘/n’,调用函数fflush(stdout),发生标准输入
,但注意printf能打印的格式是有限制的,cout可以打印重载了<<运算符的对象。 - c++20中出现了std::format,太新了先不用
- 也可以cout时加个互斥锁。。。
- 换行符直接写到字符串里,但cout刷新缓冲不支持,可能不能及时输出,因为\n在cout中不会刷新,刷新时机:
- 解决方法就是在接收线程那cout尽量改为printf,主线程使用cout就不用改了(也比较多)。为什么说尽量,因为有的cout不用换行。
服务器命令解析问题-gdb调试
在chatting的一方exit后服务器崩溃,出现Segmentation fault (core dumped)
。需要用gdb查看core文件,首先ulimit -c unlimited
,然后在Makefile编译选项加个-g
(就是-o2那里)。
不过我还是没产生core文件,直接gdb server
,在gdb内运行程序(start
),一直n
或next
跳转到start那行代码,然后复现bug,最终发现是命令解析出了问题:
唯一的可能是:因为chatting中要退出,所以要使用@,这说明要进一步检查@这一部分。
跟踪客户端的代码,对于@命令的解析是这样的:
1 | vector<string> parse(string cmdstr) |
可以看到为了复用没有@的解析,把传进来的cmdstr的@去掉了,但是上层并没有去掉,直接发给服务器了,就出问题了。所以要么把发的去掉,要么收的时候去掉。这里使用引用的话会把换行也消了,所以打算修改服务器端的代码。
改完之后这个bug就解决了。
很小的失误都会导致崩溃
- 用户1accept后服务器没有把用户1的名字发给用户2,只发了
@#chat accept
,导致用户2访vector越界。 - 用户1accept后忘记切换状态了。
这个bug是客户端sendfile后,对端因为服务器发过来的filename是空导致取filename时vec越界崩溃。
检查发现是服务器在处理sendfile命令时获取文件名使用的:find_last_of
,打成了find_last_not_of
,这样总是找到最后一个位置,然后把filename变成空。
1 | size_t pos = filename.find_last_of("/\\");//把not去掉 |
相同ip会导致映射表冲刷。这是因为使用了交换机,发给服务器的ip是交换机的ip。
日志
- 2022-10-12:有个idea,开始设计
- 接下来两天:设计客户端状态机和初步完成代码,还有一堆烦人的课程作业
- 接下来五天:根据状态机边设计边实现若干业务处理函数,还有一堆烦人的课程作业
- 接下来大概三天:完善最后的逻辑补充,如一些特殊情况的思考,以及补充一些边写边想起来的命令(config那些),还有一堆烦人的课程作业
- 接下来四天:实现服务端的设计,复杂度主要在用户映射和逻辑处理的那块,还有一堆烦人的课程作业
- 2022-10-26:客户端和服务端都完成,由于代码在markdown中手写,有少量warning和error,迅速改完后已经能成功跑起来了
接下来两三天:测bug,还是有一些问题,内存越界啊cout多线程安全,都是小事(一查查半天hh),调试后能正确运行,还有一堆烦人的课程作业 - 2022-10-30:基于tcp的p2p文件传输无响应,猜测是内网无法连接,准备进行内网穿透的实现,从NAT的类型开始了解,进行了简单的udp打洞测试(基于c++)
- 2022-10-31:udp打洞逐渐深入,实现了逆向连接(NAT和公网客户端),由于网络资源不允许(大多数人使用校园网是对称型,这种无法穿透),还无法进行双NAT下的udp打洞内网穿透;测试了tcp逆向连接的完成(基于conntrack连接跟踪原理,穿透了防火墙),这个过程中发现了TCP同时打开的现象(双主动connect实现tcp连接建立)
- 2022-11-1:周二课多,没写代码。系统了解了TCP同时打开的原理,整理了一篇博客(技术讲解博客确实写得少)
- 接下来三天:这几天没怎么干,有一堆烦人的课程作业,然后周五考试,考完下午散了下心
- 2022-11-5:借助小薛的路由器(NAT是圆锥型,可以穿透)验证了udp打洞的内网穿透;但基于tcp的内网穿透一直无法实现。
- 接下来两天:打算用quic实现udp可靠文件传输。试着配msquic环境,然而微软这个文档写的真逆天,作者测试也不完全,网上也没有相关的配置博客,折磨了两天放弃了(win10的TLS1.3打开了也test失败)
- 接下来两天:事情多,课多以及写课程大作业…
- 2022-11-9:不打算用quic了,看了其他RDT的UDP,有UDT和RUDP,GitHub上看开源项目看了好久,不太热门的东西文档太烂了,而且接口也不写明白;还看了下别人实现的简单的RDT的UDP,写的太烂了。浪费一中午和一下午时间和一个傍晚的时间,急,项目被卡着快两周了。还是要整理下心情
- 2022-11-10:打算自己写一个RDT的UDP,并且不参考tcp而参考quic,当然只是简单实现。估计要写很久了,接下来事情好多。
- 2022-11-20:十天过去了,下半学期开始后好忙…
- 2022-11-27:终于完成了RDT的UDP实现,但还没融入到项目里,之后还要肝大作业,需要等寒假再完成了。
- 2022-12-18:大作业队友迟迟不干活,把项目整理完,项目开发到此为止。