1.三次握手的过程
1)第一次握手
客户端请求建立连接,客户端将标志位SYN置为1;主机A发送位码SYN = 1,ACK = 0,随机产生一个随机数seq number = j 的同步报文(也是数据包)到服务器(TCP规定SYN=1时不能携带数据但是需要消耗一个序号);客户端进入SYN_SENT状态(即等候状态),等待服务器确认,服务器由客户端发送的SYN=1到达,客户端要求建立联机;
2)第二次握手
服务端收到连接请求报文(也是数据包)后由标志位SYN=1知道客户端请求建立连接,主机B收到请求后确认联机信息;服务端将标志位SYN和ACK都置为1;向A发送确认号ack number(=客户端的seq+1=j+1)、及SYN=1,及ACK=1,随机产生一个随机数seq=k作为初始序列号的同步确认报文并将数据包发送给客户端以确认连接请求;此时服务器进入SYN_RECV状态。
3)第三次握手
主机B收到后确认收到的ack值与发送给A的seq+1是否相等和ACK是否为1来判断连接是否建立成功。客户端收到确认后,检查ack是否为J+1,ACK是否为1;如果正确 客户端将标志ACK置为1; 客户端会再次发送ack number(=Sever的seq+1=K+1)、ACK=1,并将该确认报文(数据包)发送给服务端,服务端检查ack是否k+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器进入ESTABLISHED状态,完成三次握手,随后客户端与服务器之间可以开始传输数据了。
注:理想状态下,TCP连接一旦建立,在通信双方中任何一方主动关闭连接之前,TCP连接都将被一直保持下去。
TCP客户端代码
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchohClient {
private Socket socket = null;
public TcpEchohClient(String serverIp,int serverPort) throws IOException {
//此处可以把这里的 Ip 和port直接传给socket对象。
//由于tcp是有连接的,因此socket里面就会保存好这俩信息。
//因此此处TcpEchoClient类就不必保存。
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动");
try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()
){
Scanner scannerConsole = new Scanner(System.in);
Scanner scannerNextwork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while(true)
{
//这里的流程和UDP的客户端类似
//1.从控制台读取输入字符
if (!scannerConsole.hasNext()){
break;
}
System.out.println("-> ");
String request = scannerConsole.next();
//2.把请求发给服务器,这里需要使用println来发送,为了让发送请求的末尾带有\n
//这里是和服务器的scanner.next 呼应的
writer.println(request);
//通过flush主动刷新缓存区里的数据,以保证及时发出
writer.flush();
//3.从服务器读取相应
String response = scannerNextwork.next();
//4.把响应显示出来
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchohClient client = new TcpEchohClient("127.0.0.1",9000);
client.start();
}
}
TCP服务器代码
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
public ServerSocket serverSocket = null;
public TcpEchoServer(int port)throws IOException{
serverSocket = new ServerSocket(port);
}
public void start()throws IOException{
ExecutorService pool = Executors.newCachedThreadPool();
System.out.println("服务器启动!");
while(true){
//通过"accept" 方法来“接电话”,然后才能进行通信
Socket clintSocket = serverSocket.accept();
// Thread t = new Thread(()->{
// processConnection(clintSocket);
// });//多线程
// t.start();
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clintSocket);
}
});
}
}
private void processConnection(Socket clintSocket) {
System.out.printf("[%s:%d]客户端上线!\n",clintSocket.getInetAddress(),clintSocket.getPort());
//循环的读取客户的请求并返回响应
try(InputStream inputStream = clintSocket.getInputStream();
OutputStream outputStream = clintSocket.getOutputStream()){
while(true){
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//读取完毕,客户端断开连接就会产生读取完毕
System.out.printf("[%s:%d]客户端上下线!\n",clintSocket.getInetAddress(),clintSocket.getPort());
break;
}
//1.读取请求并解析,这里注意隐藏的约定,next读的时候要读到空白符才会结束。
//因此要求客户端发来的请求必须带有空白符结尾,比如\n或者空格
String request = scanner.next();
//2.根据请求计算相应
String response = process(request);
//3.把响应返回给客户端
//通过这种方式可以写回,但是这种方式不方便给返回的相应中写回中添加\n
//outputStream.write("[%s:%d]req: %s,resp: %s\n",clintSocket.getInetAddress(),clintSocket.getLocalPort()
// request,response);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s:%d]req: %s,resp: %s\n",clintSocket.getInetAddress(),clintSocket.getLocalPort()
,request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
clintSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9000);
server.start();
}
}
思路:
TCP提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的就是同步连接双方的序列号和确认号并交换TCP窗口大小信息。
流程:
~第一次握手(发送连接请求报文SYN=1,初始序号随机seq=想,ACK=0)发送完毕后,客户端就进入SYN_SENT状态。
~第二次握手(发送连接确认报文STN=1,ACK=1,seq =y,ACKnum=x+1)发送完毕后,服务器就进入SYN_RCV状态。
~第三次握手(发出连接确认报文ACK=1,ACKnum=y+1,序号seq=x+1)发送完毕后,客户端进入ESTABLISHED状态,当服务器接收到这个包时,也进入ESTABLISHED状态。
注:ACK也好,ack也好只不过是个代号而已。
ACK是确认值(Acknowledgement)为1便是确认连接。
ack是确认编号(Acknowledgement Number)即接受到上一次远端主机传来的seq然后+1,再发送给远端主机。提示远端主机已经成功接收上一次所有数据。

2.常见问题
1.TCP三次握手原因,为什么不是两次
主要有三个原因;
第一个原因:三次握手才能让双方均确认自己和对方的发送和接收能力都正常
1.第一次握手:客户端发送包,服务端收到了
根据第一次握手,服务端得出结论:客户端的发送能力、服务端的接收能力是正常的。
2.第二次握手:服务端发包,客户端收到了
根据第一次握手以及第二次握手,客户端能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的;
不过此时服务端并不能确认客户端的接收能力、以及服务端的发送能力是否正常
3.第三次握手:客户端发包,服务端收到了
根据第一次握手及第二次握手以及第三次握手,服务端能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
因此,需要三次握手才能确认双方的接收与发送能力是否正常
第二个原因:防止已过期的连接请求报文突然又传送到服务器,因而产生错误和资源浪费
为什么A还要发送一侧确认呢?这主要是为了防止已失效的连接请求报文突然又传送到了B,因而产生错误。
所谓“已失效的连接请求报文段”是这样产生的。双方两次握手即可建立连接的情况下,A发出连接请求,但因连接请求丢失而未收到确认。于是A再次重传一次连接请求。后来收到了确认建立了连接。数据传输完毕后,就释放了连接。A供发送了两个连接请求的报文段,其中第一个丢失,第二个到达了B。没有“已失效的连接请求报文段”。
现假定出现一种异常情况,即A发出的第一个连接请求报文段并没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放以后的某个时间才到B。本来这是一个已失效的报文段。但是B收到此失效的连接请求报文段后,就误认为是A有发出一次新的连接请求。于是就向A发出确认报文段,同意建立连接。假定不采用三次握手,那么只要B发出确认,新的连接就建立了。
由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据。但B却以为新的运输连接已经建立了,并一直等待A发来数据。B的许多资源就这样拜拜浪费了。
采用三次握手的办法可以防止上述现象的发生。例如在刚才的情况下,A不会向B的确认发出确认。B由于收不到确认,就知道A并没有要求建立连接。
第三个原因:告知对方的初始号值,并确认收到对方的初始序号值
TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。
2.TCP三次握手原因,为什么不是四次
因为三次握手已经可以确认双方的发送接收能力正常,双方都知道彼此已经准备好,而且也可以完成对双方初始序号值得确认,也就无需第四次握手了。
第一次握手:服务端确认“自己收、客户端发”报文功能正常。
第二次握手:客户端确认“自己发、自己收、服务端收、客户端发”报文功能正常,客户端认为连接已建立。
第三次握手:服务端确认“自己发、客户端收”报文功能正常,此时双方均建立连接,可以正常通信。
3.TCP三次握手能携带数据吗
其实第三次握手是可以携带数据的,但是第一次和第二次是不能携带数据的
原因:假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,而后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。
4.三次握手连接阶段,最后一次ACK包丢失,会发生什么
服务端:
第三次的ACK在网络中丢失,那么服务端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便客户端重新发送ACK包。
如果重发指定次数之后,仍然未收到 客户端的ACK应答,那么一段时间后,服务端自动关闭这个连接。
客户端:
客户端认为这个连接已经建立,如果客户端向服务端发送数据,服务端将以RST包(Reset,标示复位,用于异常的关闭连接)响应。此时,客户端知道第三次握手失败。



6402

被折叠的 条评论
为什么被折叠?



