搞cpp,网络知识还是必不可少的,学了半天还是得写出来梳理一下过程。 都是水文,个人见解哈哈!
前言 TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。 ——不用想,这就是百度的。
正文 实际上有个理想的osi七层模型(自上向下)
应用层 - 是下几层最终汇成常见的协议http、ftp等
表示层 - 其实就是展示给人看的了,也就是各种文件格式
会话层 - 对于进程,也就是正在运行的会话
传输层 - 传输单位是包
网络层 - 传输单位是报文
数据链路层 - 传输单位是帧
物理层 - 最底层,传输单位是比特(bit)
对于tcp/ip模型分为四层或五层
应用层:也就是把表示层和会话层合并了
传输层
网络层
网络接口层:此处兼并了物理层和数据链路层,所以会有五层的说法
对于目前所需,显然是TCP和UDP,并且这俩这是传输层的协议 关于详细的分层,包括淦神魔的自行检索
安全隐患 比较常见的应该就是ARP欺骗和ICMP欺骗了 这俩都是属于网络层的协议
ARP欺骗 arp是根据ip地址获取物理地址(MAC地址)的一个协议,由于arp协议没有状态,不管有没有收到都会自动缓存,所以通过这种机制,攻击方发送假的arp数据包给目标主机,从而引导特定的流量到攻击方所设置的地方。
ICMP欺骗 这玩意也差不多,主要用来提供错误报告,发现错误就返回主机,常见的ping命令就是基于ICMP的。也就有了DOS攻击这么个玩意,主机在长时间发送大量ICMP包的情况下造成cpu资源消耗,从而导致系统瘫痪,不过目前的服务器都很吊了,加上可以对这种ip直接进行封禁,所以又诞生了ddos攻击,也就是分布式的dos攻击。可谓是物理打击最为致命,分布的情况下你也不能一棒子全部打死,只能采取流量分析、氪金打造硬件之类的解决,其实也不算真正解决吧。
这也是近年来网安发展这么快的原因,后面还有各种防火墙,虽然真正有技术的还是少部分人吧,大多都是常规软件扫漏,分析日志和打打比赛。
介绍TCP/UDP
tcp协议是可靠的,稳定的,面向连接
udp协议是不可靠的,不稳定的,面向无连接
对于连接
tcp在传输数据前要先建立连接
udp则是不需要连接的,只管发
服务对象
tcp的特性让它只能一对一的两点服务
udp支持一对一、一对多、多对多的交叉
可靠性
tcp是可靠交付数据,保证数据无差错、不丢失、不重复、按需到达
udp就是尽最大努力交付,不保证数据可靠性
拥塞、流量控制
tcp有拥塞和流量控制机制,保证数据安全
udp没有,所以网络堵不堵塞也不影响这老小子发送
首部开销 因为传输层的传输单位称为包
,包的头部,也称首部,存放了相应的信息
tcp的首部长度最小占用20个字节,其中tcp的包有一个选项字段,是可选的,如果选用了,那么这个包头肯定大于20个字节
udp的首部长度占用8个字节,固定不变。
关于tcp和udp的包结构
分片机制
tcp的数据如果大于mss大小,就会在传输层的时候分片,当主机收到后在传输层进行组装,如果丢片了,就重写发这一部分
udp的数据如果大于mtu大小,则会在网络层分片,主机收到后也同样在网络层组装,再由传输层转发。
MTU:maximum transmission unit,最大传输单元,由硬件规定,如以太网的MTU为1500字节。 MSS:maximum segment size,最大分节大小,为TCP数据包每次传输的最大数据分段大小,一般由发送端向对端TCP通知对端在每个分节中能发送的最大TCP数据。MSS值为MTU值减去IPv4 Header(20 Byte)和TCP header(20 Byte)得到
二者应用场景 所以对于可靠的tcp而言,常见的服务有
ftp 文件传输服务
http/https web服务
SMTP/POP3 邮件服务
对于udp这个老小子随时发送的特性,多用于
对于可不可靠个人感觉还是相对的概念,tcp也不见得一定稳,毕竟描述都是理想情况下
了解一下常用协议的信息
协议
名称
默认端口
底层协议
HTTP
超文本传输协议
80
TCP
HTTPS
超文本传输安全协议
443
TCP
Telnet
远程登录服务的标准协议
23
TCP
FTP
文件传输协议
20传输和21连接
TCP
TFTP
简单文件传输协议
21
UDP
SMTP
简单邮件传输协议(发送用)
25
TCP
POP
邮局协议(接收用)
110
TCP
DNS
域名解析服务
53
服务器间进行域传输的时候用TCP 客户端查询DNS服务器时用UDP
tcp三次握手、四次挥手 三次握手 是指在建立tcp连接的时候,客户端和服务端共发送三个包,以确定双方收发正常给后面传输做准备,说白了就是ip和端口我都要! 先不说代码: 服务端首先要处于监听状态(listen) 第一次握手的时候,客户端给服务端发送一个SYN报文,等待服务器确定,客户端此时就处于一个SYN_SENT状态 第二次握手,服务器收到SYN包,确认客户端发来的SYN,同时服务端自己也发个SYN(SYN+ACK)包给客户端做应答,然后服务端处于一个SYN_RECV状态 第三次握手,客户端收到服务端的SYN包(SYN+ACK),向服务器发送确认包ACK,这个包发送完毕之后,客户端和服务端都进入一个ESTABLISHED(TCP连接成功)状态,此时双方成功建立连接。
1 2 3 4 5 6 7 8 9 百科copy来的,但是编程的时候好像感知不强啊,先放着有用到再说 (1)未连接队列 在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(seq=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于 Syn_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。 [3] (2)Backlog参数 三次握手协议 三次握手协议 表示内核为相应套接字排队的最大连接个数。SYN-ACK重传次数服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同。 [3] (3)半连接存活时间 是指半连接队列的条目存活的最长时间,也即服务器从收到SYN包到确认这个报文无效的最长时间,该时间值是所有重传请求包的最长等待时间总和。有时我们也称半连接存活时间为Timeout时间、SYN_RECV存活时间
四次挥手 第一步,因为tcp属于全双工状态,所以服务端和客户端都可以发起,此处以ab区分,主机a的tcp数据发完之后,向主机b发送一个带FIN标记的报文,说我东西发完了啊 第二步,主机b收到这个FIN报文,它不会立即关闭,而是发个ACK给主机a,问他确认要关闭了吧? 然后主机b处于close_wait状态等着 第三步,主机a收到ack之后,在发送一个FIN报文,告诉他我要彻底关闭了!你也可以断了! 第四步,主机b收到了这个FIN报文之后,给主机发个ACK,说那没事了,我真的关了,然后大概有一个2MSL的延迟等待之后,彻底over。
MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃
我自己不专业的理解:
关于握手为啥要三次,也是确认客户端和服务端收发一切正常。通过第一次握手,客户端发送SYN,能知道客户端可以发送,服务端可以接收;第二次握手的时候服务端会发送自己的SYN+ACK给客户端,那么服务端的收发都ok;如果服务端没有收到,应该是有重传机制的,具体多少次之后还没有反应,那么服务端就要关闭连接,关闭端口,以此减少开销。最后还要确认客户端的发送;所以第三握手的时候,客户端发送ACK应答。
关于挥手为什么要四次,感觉也是跟这个报文有关,连接的时候通过SYN+ACK嘛,SYN是用来建立同步的,ACK是用来应答的。而挥手所使用的FIN则是表示关闭的,配合ACK。当然收到FIN报文之后,socket其实不会立刻关闭,我们得给他一个时间,最合理的时机就是通过一问一答,拆分两次来完成挥手。
TCP首部中有6个标志比特,它们中的多个可同时被设置为1,主要是用于操控 TCP 的状态机的, 依次为 URG,ACK,PSH,RST,SYN,FIN 所以说上述中提到的SYN和FIN和ACK都是置为1的时候才说明连接没有问题
更细的概念请自行查阅文档,因为他们有介绍这个码位的细节,严格来说是tcp包的设计。同样的因为编程语言能控制的没有这么深,所以自己没能完全看进去,属于点到为止了
我也有参考这篇文章
代码示例
模拟的话肯定是要模拟c/s架构也就是客户端和服务器
cpp的socket编程,不了解的先翻阅资料,我们称为套接字
tcp和udp是不同的协议,所以实现也不相同
**: error C4996: 'inet_addr': Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings**
出现这个问题,直接在项目属性-c/c++-预处理器-预处理器定义里面把_WINSOCK_DEPRECATED_NO_WARNINGS
添加进去,记得用分号隔开。
或者在源文件里面开头加一句#pragma warning(disable:4996)
tcp的demo
tcp的服务端需要的操作
创建套接字socket
绑定客户端地址信息socketaddr_int和bind
监听连接listen
accept,也就是从监听的队列取出第一个然后连接
收发 recv和send
客户端需要的操作
创建套接字
设置服务器地址信息
连接客户端 connect
收发 recv和send
套接字需要手动关闭,包括socket环境也要清理 现今所用的头文件应该都是Winsocke2.h
了 另外注意目前还是使用ipv4为主
先搞个最简单的收发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <iostream> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib" ) int main () { WSAData wsa; SOCKET m_server; SOCKET m_client; SOCKADDR_IN serAddr; if (0 != WSAStartup (MAKEWORD (2 , 2 ), &wsa)) { printf ("wsastartup error!\r\n" ); WSACleanup (); } m_server = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); if (INVALID_SOCKET == m_server) { printf ("socket error\n" ); WSACleanup (); } serAddr.sin_family = AF_INET; serAddr.sin_port = htons (7985 ); serAddr.sin_addr.s_addr = inet_addr ("127.0.0.1" ); int ret = bind (m_server, (sockaddr*)&serAddr, sizeof (SOCKADDR_IN)); if (SOCKET_ERROR == ret) { printf ("bind error\r\n" ); } ret = listen (m_server, 5 ); if (SOCKET_ERROR == ret) { printf ("listen error\r\n" ); } int cli_Size = sizeof (sockaddr_in); if (INVALID_SOCKET == (m_client = accept (m_server, (sockaddr*)&serAddr, &cli_Size))) { printf ("accept error\r\n" ); } char sendBuf[MAX_PATH] = "hello client" ; send (m_client, sendBuf, sizeof (sendBuf), 0 ); printf ("server sendbuf!\r\n" ); Sleep (1000 ); closesocket (m_server); closesocket (m_client); WSACleanup (); system ("pause" ); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib" ) int main () { WSAData wsa; SOCKET m_sock; sockaddr_in addr; if (0 != WSAStartup (MAKEWORD (2 , 2 ), &wsa)) { printf ("wsastartup error!\r\n" ); WSACleanup (); } m_sock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); if (INVALID_SOCKET == m_sock) { printf ("socket error\n" ); WSACleanup (); } addr.sin_family = AF_INET; addr.sin_port = htons (7985 ); addr.sin_addr.s_addr = inet_addr ("127.0.0.1" ); if (SOCKET_ERROR == connect (m_sock, (sockaddr*)&addr, sizeof (sockaddr_in))) { printf ("connect error!\r\n" ); } char recvBuf[MAX_PATH] = "" ; recv (m_sock, recvBuf, MAX_PATH, 0 ); printf ("client recvbuf:%s\r\n" , recvBuf); Sleep (1000 ); closesocket (m_sock); WSACleanup (); system ("pause" ); return 0 ; }
在visual studio 上跑就行,分开两个项目只是为了更好理解,如果抽象能力足够,放到一个项目下也行。 此外现在也只是单线程的工作收发就结束了还可以互动一下。
有个建议也是约定俗成的,就是if判断的时候把变量放在右边,常量放在左边。也就是上面代码if的时候的操作。虽然对于我们人类阅读有点不便,但是它可以防止最常见的错误if(a=0)
,就是判断变成赋值的时候,这样子是合法的不报错但是偏离我们意愿了。改成if(0=a)
编译器立马就能给出反应
大致就是这样服务端因为send很快就结束了,客户端则是需要一丢丢时间收到数据然后打印出来 这里演示的是服务端发送,客户端接收。 那么相应的,一收一发也很简单,加个缓冲区和过程就行。
1 2 3 4 5 6 7 8 char sendBuf[MAX_PATH] = "hello client" ; send (m_client, sendBuf, sizeof (sendBuf), 0 );printf ("server sendbuf!\r\n" );char recvBuf[MAX_PATH] = "" ;recv (m_client, recvBuf, sizeof (recvBuf), 0 );printf ("server recv:%s\r\n" , recvBuf);
1 2 3 4 5 6 7 8 char recvBuf[MAX_PATH] = "" ; recv (m_sock, recvBuf, MAX_PATH, 0 );printf ("client recvbuf:%s\r\n" , recvBuf);char sendBuf[MAX_PATH] = "hello server" ; send (m_sock, sendBuf, sizeof (sendBuf), 0 );printf ("client sendbuf!\r\n" );
甚至无聊点还可以while(true)
持续的收发收发。那么到此基础的网络就形成了
关于变量命名,驼峰和匈牙利命名法可以多学习
判断的时候右值放前面有助于写错或者忘记,虽然可能不习惯,这个无伤大雅
加个sleep是怕套接字挂的太快哈哈,实际应该没啥大事
有关参数我这也粗略描写,具体你在vs上让光标处于这个函数上然后按f1可以跳转到微软的文档,有些可能有中文,没有的就需要翻译一下了。
关于WSAData,本质是一个结构体,微软都会做一些区分,咱这默认就用两个WORD那个就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef struct WSAData { WORD wVersion; WORD wHighVersion; #ifdef _WIN64 unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; char szDescription[WSADESCRIPTION_LEN+1 ]; char szSystemStatus[WSASYS_STATUS_LEN+1 ]; #else char szDescription[WSADESCRIPTION_LEN+1 ]; char szSystemStatus[WSASYS_STATUS_LEN+1 ]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; #endif } WSADATA, FAR * LPWSADATA;
在文档中,微软也有示例:
1 2 3 4 5 6 7 8 9 10 11 12 WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD ( 2 , 2 ); err = WSAStartup ( wVersionRequested, &wsaData ); if ( err != 0 ) { return ; }
所以我们也可以直接缩写成WSAStartup(MAKEWORD(2, 2), &wsa)
。 关于版本在 wHighVersion 成员中返回的 Windows 套接字规范的当前版本 WSADATA 结构是版本 2.2,编码为低字节中的主版本号和高字节中的次要版本号。 此版本的当前 Winsock DLL (Ws2_32.dll)支持请求以下任意版本的 Windows 套接字规范的应用程序:
关于SOCKET,也是个小问题,微软会对一些类型命名成全大写的存在
1 2 3 4 5 SOCKET WSAAPI socket ( [in] int af, [in] int type, [in] int protocol ) ;
参数1位地址簇规范,即ipv4和ipv6,可能有点小区别,见文档 参数2表示套接字类型,一般都用流,反正这些基本都是预定义的宏,稍微记住就行。 参数3位使用协议,咱这因为是tcp演示,就指明是tcp了。默认为0,代表服务提供者自动协商。
关于sockaddr_in,通过代码,应该明确知道就是存放地址细节的一个结构体 参数1,根据注释此成员应该一直为AF_INET,后续的socket创建也有这个 参数2,就是端口号 参数3,就是ip的点分十进制方法
ok,只要你能自己尝试写一下,然后看看这些参数,翻阅文档,那么基本使用套接字就没啥问题了
然后就是最基本的一问一答,你也可以设置关键字退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 while (true ) { recv (m_client, recvBuf, MAX_PATH, 0 ); if (recvBuf[0 ] == 'q' ) { std::cout << "client quit!" << std::endl; break ; } printf ("client msg:%s\r\n" , recvBuf); std::cout << "server:" ; std::cin >> sendBuf; send (m_client, sendBuf, sizeof (sendBuf), 0 ); printf ("server sendbuf!\r\n" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 while (true ) { std::cout << "client:" ; std::cin >> sendBuf; send (m_sock, sendBuf, sizeof (sendBuf), 0 ); if (sendBuf[0 ] == 'q' && strlen (sendBuf) <= 1 ) { printf ("client quit!\r\n" ); break ; } printf ("client sendbuf!\r\n" ); recv (m_sock, recvBuf, MAX_PATH, 0 ); printf ("server msg:%s\r\n" , recvBuf); }
相似的步骤不做赘述,只看看关键的地方 除了基础类型char,也可以使用cpp的string类,那个自带个size倒是方便点 还有就是,服务端的特性,他不会主动连接,所以对于send,第一次肯定是客户端发起的,服务器启动后的事就是等待消息
比较抱歉的就是,c和cpp混用习惯了写代码老会混用,会努力改变这个不好的习惯
上述的操作都是一对一,服务端没有办法同时连接多个客户端,最好的办法就是通过多线程,服务端能处理更多的连接。 多线程此处就不做演示了。
udp的demo udp的服务器
创建套接字
bind
recvfrom
sendto
关闭套接字
udp的客户端
创建套接字
sendto
recvfrom
关闭套接字
相比tcp,udp不需要监听然后去等连接请求,而是绑定完端口ip之后直接收发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib" ) int main () { SOCKET m_sock; SOCKADDR_IN m_addr; SOCKADDR_IN m_cliAddr; int m_cliAddrLen = 0 ; WSADATA wsa; if (0 != WSAStartup (MAKEWORD (2 , 2 ), &wsa)) { std::cout << "wsastartup error!\r\n" ; return 0 ; } m_sock = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (INVALID_SOCKET == m_sock) { closesocket (m_sock); m_sock = INVALID_SOCKET; return 0 ; } m_addr.sin_family = AF_INET; m_addr.sin_port = htons (9527 ); m_addr.sin_addr.s_addr = inet_addr ("127.0.0.1" ); int ret = bind (m_sock, (sockaddr*)&m_addr, sizeof (sockaddr_in)); if (SOCKET_ERROR == ret) { closesocket (m_sock); m_sock = INVALID_SOCKET; return 0 ; } char recvBuf[1024 ] = { 0 }; char sendBuf[1024 ] = "由服务端发送" ; recvfrom (m_sock, recvBuf, 1024 , 0 , (sockaddr*)&m_cliAddr, &m_cliAddrLen); std::cout << "server recv:" << recvBuf << std::endl; sendto (m_sock, sendBuf, sizeof (sendBuf), 0 , (sockaddr*)&m_cliAddr, m_cliAddrLen); std::cout << "server send success!\r\n" ; closesocket (m_Socket); WSACleanup (); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> #include <WinSock2.h> #pragma comment(lib, "ws2_32.lib" ) int main () { SOCKET m_sock; SOCKADDR_IN m_addr; int m_addrLen = 0 ; WSADATA wsa; if (0 != WSAStartup (MAKEWORD (2 , 2 ), &wsa)) { std::cout << "wsastartup error!\r\n" ; return 0 ; } m_sock = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (INVALID_SOCKET == m_sock) { closesocket (m_sock); m_sock = INVALID_SOCKET; return 0 ; } m_addr.sin_family = AF_INET; m_addr.sin_port = htons (9527 ); m_addr.sin_addr.s_addr = inet_addr ("127.0.0.1" ); char recvBuf[1024 ] = { 0 }; char sendBuf[1024 ] = "由客户端发送" ; sendto (m_sock, sendBuf, sizeof (sendBuf), 0 , (sockaddr*)&m_addr, m_addrLen); std::cout << "client send success!\r\n" ; recvfrom (m_sock, recvBuf, 1024 , 0 , NULL , NULL ); std::cout << "client recv:" << recvBuf << std::endl; system ("pause" ); closesocket (m_Socket); WSACleanup (); return 0 ; }
这也是最基础的一次性连接过程,而且还是在本地的。
循环参考上面tcp那种就行。
多线程自行解决!!!!
能优化的地方还有很多,像send和recv都是有返回值的,可以做校验。
c++ socket编程 网上的文章有很多可以参考的。
先实现通信,在考虑传输文件或者数据。
可参考文章
总结 总结不完~ 单独讲都能讲一堆,虽然也能简化。 不过对于这种网络模型,不管是osi理想的七层,还是tcp/ip都有各自的缺点。