原理
1、 Socket
Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP为网络服务和应用提供Socket编程接口,当前主要的Socket编程主要有SOCK_STREAM (TCP)、SOCK_DGRAM (UDP) 工作在传输层,SOCK_RAW 工作在网络层。
2、 TCP报文封装及通信过程
TCP在IP层提供的不可靠服务基础上实现可靠数据传输服务,流水线机制传输,使用累积确认号确认传输,并使用单一重传定时器和收到重复ACK确认传输失败,进行重传。
TCP 段结构包含。
源地址端口、目的端口,16位字段,发送接收该报文段的主机中应用程序的端口号。
序号(segment第一个字节的编号)、确认号(接收方期望从对方接受的字节编号), Flag(URG:紧急数据标志位;ACK:确认标志位;PSH:请求推送位,发送了数据;RST:连接复位;SYN:建立连接,让连接双方同步序列号;FIN:释放连接)。
窗口大小: TCP的窗口大小,以字节为单位。最大长度是65535字节(16位)。
检验和:将传输层传输层伪首部与首部字段求和并校验,保证数据的完整性和准确性。
主要内容以及使用的设备以及软件
前期准备
1、安装有 Wireshark 的客户端;
2、使用Java/C/C++/C#/Python 等语言编写Socket 通信程序。
3、安装并配置能运行的服务器及客户端环境
4、基于TCP 的 SOCKET 通信测试及验证。
5、TCP 通信过程分析
主要内容
1、端口扫描编程及验证
端口扫描是基于Socket函数的应用,一般通过Socket connect连接服务器端口,建立成功,就说明对方开放了该端口,对于了解服务器开启了那些网络服务比较有用。
目前主要的扫描有TCP connect() 、TCP SYN、TCP FIN等,这些扫描对于真实的生产环境的网络安全造成一定的威胁,扫描的测试及验证应自己搭建虚拟服务器进行。
2、Socket通信编程
当前主要的Socket编程主要有SOCK_STREAM (TCP)、SOCK_DGRAM (UDP),SOCK_RAW,要求完成基于TCP 的Socket通信,包括server和client部分,用C、C++、C#、JAVA和Python实现都可以,必须有连接建立,数据传输及连接拆除过程。
3、Socket 通信测试
Socket通信过程可以在真实的网络环境中测试,也可在本地虚拟网络中测试;要求在建立连接后,互相发送学号姓名。
如果真实网络环境中,没有足够的设备,可以尝试在电脑和手机端之间进行。
测试过程示例如图:
4、传输层协议及通信过程分析
为了验证及分析传输层协议及通信过程,应在服务器和客户端之间至少发送一条数据(建议包含学号和姓名),并且由退出及关闭连接,在SocketServer或者SocketClient所在的网卡进行报文捕捉,报文内容应包含三次握手建立连接过程、数据发送及确认,四次挥手拆除连接过程。
示例如下:
5、Segment 分段测试验证
准备一个大于1460字节2倍数以上的字符串,进行TCP传输,在服务器或客户端捕捉报文,并进行分析。
示例如图,准备了一个2950的字符串,每1460提示OK结束,可以看到数据2950被分为3个segment传输,分别为1460,1460,30,确认号是在序列号基础上加2950,
6、Segment 分段测试验证
准备一个大于1460字节2倍数以上的字符串,进行TCP传输,在服务器或客户端捕捉报文,并进行分析。
示例如图,准备了一个2950的字符串,每1460提示OK结束,可以看到数据2950被分为3个segment传输,分别为1460,1460,30,确认号是在序列号基础上加2950,具体过程如图。
7、RST测试验证
RST表示复位,用来异常的关闭连接。接收端收到RST包后,不必发送ACK包确认。
TCP处理程序会自动判断异常时刻发送RST包。如:
1)、Client向Server发起连接,但Server并未监听相应的端口,此时Client TCP处理程序会发RST包。
2)、请求超时。
3)、连接已经正常建立了,通讯过程中,Client向Server发送了FIN包要求拆除连接,Server发送ACK确认后,Server并未发起Fin 来进行第三次挥手,这时Client处于time wait阶段,而Server尚未关闭Socket,这时Server发送数据给Client,就会出现RST。
分析我们捕捉到的报文,符合第三种情况。产生了RST。
8、WEB SOCKET编程
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。
HTTP协议是单向的网络协议,建立连接后只允许浏览器向服务器发出请求后,服务器才能返回相应的数据。在即时通讯时,轮询时间间隔,进行交互。缺点是需要不断的发送请求,而且通常由实验一我们知道HTTP request的Header是非常长的,为了传输一个很小的数据需要附加过长的控制信息,传输成本和负载过大。
WebSocket需要服务器和浏览器通过HTTP协议进行一个握手的动作,然后单独建立一条TCP的通信通道进行数据的传送,减少传输成本和负载。
验证
1、端口扫描编程及本地服务器测试
1.1 端口扫描
注意:
端口扫描编程及本地服务器测试的时候自己电脑很多端口是未开启的状态。
解决方法:
可以通过cmd输入netstat查询。(netstat命令的功能是显示网络连接、路由表和网络接口信息,可以让用户得知有哪些网络连接正在运作。)
c语言方法
#include<WinSock2.h> #include<iostream> #define DEST_IP "127.0.0.1" //IP地址 #define DEST_PORT m //端口 using namespace std; int m; int main() { WORD mVersionRequested = MAKEWORD(2,2); WSADATA wsaData; if(WSAStartup(mVersionRequested,&wsaData) != 0){ cout<<"初始化 WinSock 失败!"<<endl; return 0; } int start,stop; cout<<"请输入目的主机的扫描起始端口:"; cin>>start; cout<<"请输入目的主机的扫描结束端口:"; cin>>stop; for(m=start;m<=stop;m++){ int sockfd,n; struct sockaddr_in dest_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); dest_addr.sin_family = AF_INET; dest_addr.sin_port = htons(DEST_PORT); dest_addr.sin_addr.s_addr = inet_addr(DEST_IP); cout<<"正在扫描端口:"<<m<<endl; n = connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)); if(n==-1){ cout<<m<<"端口没有开启"<<endl; } else{ cout<<m<<"端口开启"<<endl; } } return 0; }
端口扫描结果
Python方法:
class ScanPort: def __init__(self): self.ip = None def scan_port(self, port): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) res = s.connect_ex((self.ip, port)) if res == 0: # 端口开启 print('Ip:{} Port:{} IS OPEN\n'.format(self.ip, port)) else: print('Ip:{} Port:{}: IS NOT OPEN\n'.format(self.ip, port)) except Exception as e: print(e) finally: s.close() def start(self): remote_server = input("输入要扫描的远程主机:") self.ip = socket.gethostbyname(remote_server) ports = [i for i in range(1, 1025)] socket.setdefaulttimeout(0.5) # 开始时间 t1 = datetime.now() # 设置多进程 threads = [] pool = ThreadPool(processes=8) pool.map(self.scan_port, ports) pool.close() pool.join() print('端口扫描已完成,耗时:', datetime.now() - t1) ScanPort().start()
端口扫描结果
1.2 wireshark抓包分析
1.2.1 TCP三次握手过程分析
TCP三次握手
TCP三次握手过程分析
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
(1)第一次握手:客户端给服务端发一个 SYN 报文,并进入SYN_SEND状态,等待服务器确认。(SYN->1,其他为0)
此时首部的同步位SYN=1,初始序号seq=x(0),SYN=1的报文段不能携带数据,但要消耗掉一个序号。
第一次握手
(2)第二次握手:服务器收到syn包,必须确认客户的SYN,同时自己也发送一个SYN包,即SYN+ACK包,此时服务器进入SYN_RECV状态。(ACK->1,SYN->1,其他为0)
在确认报文段中SYN=1,ACK=1,确认号ack=x+1(1),初始序号seq=y(0)。
第二次握手
(3)第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK,此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。(ACK->1,其他为0)
确认报文段ACK=1,确认号ack=y+1(1),序号seq=x+1(1)(初始为seq=x(0),第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
第三次握手
1.2.2 TCP四次挥手过程分析
TCP四次挥手
TCP四次挥手过程分析
刚开始双方都处于ESTABLISHED状态,当客户端先发起关闭请求。
(1)第一次挥手:主动关闭方(客户端)发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了,但是,此时主动关闭方还可以接受数据。此时客户端处于FIN_WAIT1状态。(ACK->1,FIN->1)
即连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
第一次挥手
(2)第二次挥手:服务端收到FIN之后,会发送ACK报文,且把客户端的序列号值+1作为ACK报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT状态。(ACK->1)
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
第二次挥手
(3)第三次挥手:被动关闭方(服务端)发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。此时服务端处于LAST_ACK的状态。(ACK->1,FIN->1)
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
第三次挥手
(4)第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手,此时客户端处于 TIME_WAIT 状态。(ACK->1)
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间后,客户端才进入CLOSED状态。
第四次挥手
2、Socket通信编程代码
我使用了C语言和python两种语言实现Socket通信。
C语言代码:
//客户端 #include <stdio.h> #include <WinSock2.h> //windows socket的头文件 #include <Windows.h> #include <iostream> #include <thread> #include <process.h> #pragma comment(lib, "ws2_32.lib") //连接winsock2.h的静态库文件 using namespace std; int main() { //加载winsock库 WSADATA wsadata; WSAStartup(MAKEWORD(2, 3), &wsadata); //客户端socket SOCKET clientSock = socket(PF_INET, SOCK_STREAM, 0); //初始化socket信息 //memset:作用是在一段内存块中填充某个给定的值,它对较大的结构体或数组进行清零操作的一种最快方法。 sockaddr_in clientAddr; memset(&clientAddr, 0, sizeof(SOCKADDR)); //设置Socket的连接地址、方式和端口 clientAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); clientAddr.sin_family = PF_INET; clientAddr.sin_port = htons(2017); //建立连接 connect(clientSock, (SOCKADDR*)&clientAddr, sizeof(SOCKADDR)); cout << "已建立连接。" << endl; //发送消息 char* s = new char[100]; cout << "请输入你要发送的文字消息: "; cin >> s; send(clientSock, s, strlen(s) * sizeof(char) + 1, NULL); cout << "已发送:" << s << endl; //接收消息 system("pause"); char Buffer[MAXBYTE] = { 0 }; recv(clientSock, Buffer, MAXBYTE, 0); cout << "通过端口:" << ntohs(clientAddr.sin_port) << "接收到:" << Buffer << endl; //关闭连接 closesocket(clientSock); WSACleanup(); cout << "客户端连接已关闭。" << endl; system("pause"); return 0; }
客户端
//服务器 #include <stdio.h> #include <WinSock2.h> //windows socket的头文件 #include <Windows.h> #include <iostream> #include <thread> #include <mutex> #include <process.h> #pragma comment(lib, "ws2_32.lib") //连接winsock2.h的静态库文件 using namespace std; //mutex 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。 //同一时刻,只能有一个线程持有该锁。 mutex m; //定义结构体用来设置 typedef struct my_file { SOCKET clientSocket; //文件内部包含了一个SOCKET 用于和客户端进行通信 sockaddr_in clientAddr; //用于保存客户端的socket地址 int id; //文件块的序号 }F; DWORD WINAPI transmmit(const LPVOID arg) { //实际上这里为了追求并发性不应该加锁,上锁是为了方便看输出 m.lock(); F* temp = (F*)arg; //获取文件的序号 //int file_id = temp->id; //获取客户机的端口号 //ntohs(temp -> clientAddr.sin_port); cout << "测试开始,等待客户端发送消息..." << endl; //从客户端处接受数据 char Buffer[MAXBYTE] = { 0 }; //缓冲区 recv(temp->clientSocket, Buffer, MAXBYTE, 0); //recv方法 从客户端通过clientScocket接收 cout << "线程" << temp->id << "从客户端的" << ntohs(temp->clientAddr.sin_port) << "号端口收到:" << Buffer << endl; //发送简单的字符串到客户端 const char* s = "Server file"; send(temp->clientSocket, s, strlen(s) * sizeof(char) + 1, NULL); cout << "线程" << temp->id << "通过客户端的" << ntohs(temp->clientAddr.sin_port) << "号端口发送:" << s << endl; m.unlock(); return 0; } int main() { //加载winsock库,第一个参数是winsocket load的版本号(2.3) WSADATA wsaData; WSAStartup(MAKEWORD(2, 3), &wsaData); //创建服务器端的socket(协议族, sokcet类型) SOCKET servSocket = socket(AF_INET, SOCK_STREAM, 0);//如果改成SOCK_DGRAM则使用UDP // 初始化socket信息 sockaddr_in servAddr; //服务器的socket地址,包含sin_addr表示IP地址,sin_port保持端口号和sin_zero填充字节 memset(&servAddr, 0, sizeof(SOCKADDR)); //初始化socket地址 //设置Socket的连接地址、方式和端口,并绑定 servAddr.sin_family = PF_INET; //设置使用的协议族 servAddr.sin_port = htons(2017); //设置使用的端口 servAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //设置绑定的IP地址 ::bind(servSocket, (SOCKADDR*)&servAddr, sizeof(SOCKADDR)); //将之前创建的servSocket和端口,IP地址绑定 HANDLE hThread[20]; //获取句柄 listen(servSocket, 20); //监听服务器端口,最多20个连接 for (int i = 0; i < 20; i++) { F* temp = new F; //创建新的传输结构体 sockaddr_in clntAddr; int nSize = sizeof(SOCKADDR); SOCKET clientSock = accept(servSocket, (SOCKADDR*)&clntAddr, &nSize); //temp数据成员赋值 temp->clientSocket = clientSock; temp->id = i + 1; temp->clientAddr = clntAddr; //通过句柄创建子线程 hThread[i] = CreateThread(NULL, 0, &transmmit, temp, 0, NULL); } //等待子线程完成 WaitForMultipleObjects(20, hThread, TRUE, INFINITE); cout << WSAGetLastError() << endl; //查看错误信息 //关闭socket,释放winsock closesocket(servSocket); WSACleanup(); cout << "服务器连接已关闭。" << endl; system("pause"); return 0; }
服务端
注意运行报错可能原因:
端口可能被防火墙阻止: 可以去检查防火墙设置,确保你尝试连接的端口没有被防火墙阻止
Winsock版本或者网络设置问题:你可以在命令行中使用netsh winsock show命令来查看你的Winsock版本和设置。
修改编译器选项:
Python代码:
# 导入 socket模块 import socket # 创建一个socket对象 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host = "连接者电脑的ipv4" port = 7777 # host = "10.225.146.20" # port = 8888 # 连接服务端 print("Socket通信客服端") client.connect((host, port)) while True: send_msg = input("发送: ") # 设置退出条件 if send_msg == "q": break send_msg = send_msg # 发送数据,编码 client.send(send_msg.encode("utf-8")) # 接收服务端返回的数据 msg = client.recv(1024) # 解码 print("接收:%s" % msg.decode("utf-8")) # 关闭客户端 client.close()
客户端
import socket # 创建一个socket对象 socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host = "自己电脑的ipv4" port = 8888 # 10.225.150.102 # port = 8888 # 绑定地址 socket_server.bind((host, port)) # 设置监听 socket_server.listen(5) # socket_server.accept()返回一个元组, 元素1为客户端的socket对象, 元素2为客户端的地址(ip地址,端口号) client_socket, address = socket_server.accept() print("Socket通信服务器端") # while循环是为了让对话持续 while True: # 接收客户端的请求 recvmsg = client_socket.recv(1024) # 把接收到的数据进行解码 strData = recvmsg.decode("utf-8") # 设置退出条件 if strData == 'q': break print("接收: %s" % strData) # 输入 msg = input("发送: ") # 发送数据,需要进行编码 client_socket.send(msg.encode("utf-8")) # 关闭服务器端 socket_server.close()
服务端
注意 运行报错可能原因:
要将地址改成自己和连接者的ipv4 且 需要再同一网络下进行
Error: [WinError 10013] 以一种访问权限不允许的方式做了一个访问套接字的尝试:
django运行时的端口被其他服务占用了,此时需要关闭占用端口的服务,
django的默认端口为8000
解决办法1
1.netstat -ano | findstr 8000 // cmd操作获取占用id
2.taskkill /pid 23824 /F // 关闭端口。
3.netstat -ano | findstr 8000 // 8000端口被重新占用
4.tasklist | findstr 40644 // 查看占用程序名
5.taskkill /f /t /im KGService.exe // 杀掉程序 ,也可直接关闭该应用程序
6.python manage.py runserver // 启动Django服务
解决办法2
python manage.py runserver 端口号 // 换个端口
3、进行socket通信测试,收到来自服务器端发来的消息
C语言:(可以在一台电脑通过服务器和客户端发送和接收信息)
客户端发送信息
服务器接收信息
Python:(需要同学合作,一个同学使用服务端的时候另一个同学需要使用客户端)
客户端发送信息
服务器接收信息
4、传输层协议及通信过程分析
为了验证及分析传输层协议及通信过程,在服务器和客户端之间发送了两条数据(包含学号和姓名),并且由退出以及关闭连接。
服务端运行截图
对socket通信信息进行报文捕捉
分析过程及序列号和确认号计算:
通过筛选端口8888进行报文捕捉
分析:
- 通过TCP三次握手:
第一次握手seq=0。
第二次握手seq=0,ack=seq(第一次)+1=1。
第三次握手seq=seq(第一次)+1=1,ack=seq(第二次的)+1=1。 - 客户端发送 “20215120807lyh” 长度为14的字符串
此时[PSH,ACK] seq=1,ack=1,len=14
并且服务端返回ACK=1表示确认收到,此时seq=1,ack=1+14=15。 - 服务端发送 “20215120808ZSJ” 长度为14的字符串
此时[PSH,ACK] seq=1,ack=15,len=14
并且客户端返回ACK=1表示确认收到,此时seq=15,ack=15。 - 客户端向服务端发送断开请求以及服务端发送断开请求。(四次挥手)
第一次挥手seq=15,ack=15。
第二次挥手seq=ack(第一次)=15,ack=seq(第一次)+1=16。
第三次挥手seq=seq(第二次)=15,ack=ack(第二次)=16。
第四次挥手seq=ack(第三次)=16,ack=seq(第三次)+1=16。
分析过程及序列号计算示例如图所示
5、Segment分段测试验证
①准备一个大于 1460 倍数的字符串:
准备的字符串长度:14+2*1460=2934(学号加姓名缩写占14,“aa”1460占 2* 1460=2920)
通过python程序准备一个大于 1460 倍数的字符串
②进行 TCP 传输,在服务器端捕捉报文,分析分段情况:
情况1:发送一个长度为2934的字符串,分了一段。
TCP报文截图
情况2:发送一个长度为2934的字符串,分了3段,分别是1460,1460,14。
TCP报文截图
③进行 UDP传输,在服务器端捕捉报文,分析分段情况:
UDP报文截图
udp分析:
1没有三次握手
2 UDP和网络层只提供尽力而为的服务,客户端未必能接收(无法分解到进程)
6、RST测试验证
重连发生RST的抓包情况:
发送数据时,因为服务端还未完全接收到缓冲区的数据而客户端断开连接可造成RST包(RST->1,ACK->1)。
重连发生RST
7、WEB SOCKET编程
分析:
服务端其实就是一个TCP服务器。
服务端的实现就是网络编程,具体过程收到客户机的请求,服务器端响应(将 HTML文本发送给游览器,游览器识别之后进行展示和响应的信息)。
项目结构:
Handler.Java:线程类,通过多线程的方式处理客户机的请求
HttpServer.Java:服务器端代码
Handler.Java代码:线程类,通过多线程的方式处理客户机的请求
package com.example.hello; import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; public class Handler extends Thread{ protected Socket socket; public Handler(Socket socket) { this.socket=socket; } @Override public void run() { try(InputStream input=this.socket.getInputStream()){ try(OutputStream output=this.socket.getOutputStream()) { handle(input,output); } catch (Exception e) { } } catch (Exception e) { try { this.socket.close(); } catch (IOException e1) { e1.printStackTrace(); } } } private void handle(InputStream input, OutputStream output) throws IOException { BufferedReader reader=new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)) ; BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(output,StandardCharsets.UTF_8)); // 读取http请求 boolean requestOk=false; String first=reader.readLine(); if (first.startsWith("GET / HTTP/")) { requestOk=true; } for(;;){ String header=reader.readLine(); // 读取头部信息为空时,HTTP Header读取完毕 if (header.isEmpty()) { break; } System.out.println(header); } System.out.println(requestOk ? "Response OK" : "Response Error"); // 请求失败 if (!requestOk) { // 发送错误响应: writer.write("HTTP/1.0 404 Not Found\r\n"); writer.write("Content-Length: 0\r\n"); writer.write("\r\n"); writer.flush(); }else { // 请求成功 // 发送成功响应: String data = "<html><body><h1>Hello, world!</h1></body></html>"; int length = data.getBytes(StandardCharsets.UTF_8).length; writer.write("HTTP/1.1 200 OK\r\n"); writer.write("Connection: close\r\n"); writer.write("Content-Type: text/html\r\n"); writer.write("Content-Length: " + length + "\r\n"); writer.write("\r\n"); // 空行标识Header和Body的分隔 writer.write(data); writer.flush(); } } }
HttpServer.Java代码:服务器端
package com.example.hello; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class HttpServer { public static void main(String[] args) throws IOException { // 监听8888端口 ServerSocket serverSocket=new ServerSocket(8888); System.out.println("server is running..."); while(true){ Socket socket=serverSocket.accept();// 监听 System.out.println("connected from " + socket.getRemoteSocketAddress()); Thread t = new Handler(socket); t.start(); } } }
Handler部分代码解释
运行结果:
运行HttpServer.Java后打开游览器,输入网址http://127.0.0.1:8888。
浏览器页面
运行结果