TCP/IP 协议简介
IP
首先我们看 IP(Internet Protocol)协议。IP 协议提供了主机和主机间的通信。
为了完成不同主机的通信,我们需要某种方式来唯一标识一台主机,这个标识,就是著名的IP地址。通过IP地址,IP 协议就能够帮我们把一个数据包发送给对方。
TCP
前面我们说过,IP 协议提供了主机和主机间的通信。
TCP 协议在 IP 协议提供的主机间通信功能的基础上,完成这两个主机上进程对进程的通信。
有了 IP,不同主机就能够交换数据。但是,计算机收到数据后,并不知道这个数据属于哪个进程(简单讲,进程就是一个正在运行的应用程序)。TCP 的作用就在于,让我们能够知道这个数据属于哪个进程,从而完成进程间的通信。
为了标识数据属于哪个进程,我们给需要进行 TCP 通信的进程分配一个唯一的数字来标识它。这个数字,就是我们常说的端口号。
三次握手
TCP 的全称是 Transmission Control Protocol,大家对它说得最多的,大概就是面向连接的特性了。之所以说它是有连接的,是说在进行通信前,通信双方需要先经过一个三次握手的过程。三次握手完成后,连接便建立了。这时候我们才可以开始发送/接收数据。(与之相对的是 UDP,不需要经过握手,就可以直接发送数据)。
下面我们简单了解一下三次握手的过程。
- 首先,客户向服务端发送一个
SYN
,假设此时 sequence number 为x
。这个x
是由操作系统根据一定的规则生成的,不妨认为它是一个随机数。 - 服务端收到
SYN
后,会向客户端再发送一个SYN
,此时服务器的seq number = y
。与此同时,会ACK x+1
,告诉客户端“已经收到了SYN
,可以发送数据了”。 - 客户端收到服务器的
SYN
后,回复一个ACK y+1
,这个ACK
则是告诉服务器,SYN
已经收到,服务器可以发送数据了。
经过这 3 步,TCP 连接就建立了。这里需要注意的有三点:
- 连接是由客户端主动发起的
- 在第 3 步客户端向服务器回复
ACK
的时候,TCP 协议是允许我们携带数据的。之所以做不到,是 API 的限制导致的。 - TCP 协议还允许 “四次握手” 的发生,同样的,由于 API 的限制,这个极端的情况并不会发生
一、socket通信基本原理
Socket 是 TCP 层的封装,通过 socket,我们就能进行 TCP 通信。
socket 通信是基于TCP/IP协议的一种传送方式,实现网络间的双向通信,我们通常把TCP和UDP称为传输层。
如上图,在七个层级关系中,我们讲的socket属于传输层,
其中UDP是一种面向无连接的传输层协议。UDP不关心对端是否真正收到了传送过去的数据。
如果需要检查对端是否收到分组数据包,或者对端是否连接到网络,则需要在应用程序中实现。
UDP常用在分组数据较少或多播、广播通信以及视频通信等多媒体领域。
在这里我们不进行详细讨论,这里主要讲解的是基于TCP/IP协议下的socket通信。
socket是基于应用服务与TCP/IP通信之间的一个抽象,他将TCP/IP协议里面复杂的通信逻辑进行分装,
对用户来说,只要通过一组简单的API就可以实现网络的连接
首先,服务端初始化ServerSocket,然后对指定的端口进行绑定,接着对端口及进行监听,通过调用accept方法阻塞,
此时,如果客户端有一个socket连接到服务端,那么服务端通过监听和accept方法可以与客户端进行连接。
二 基本示例
服务端
1 package socket.socket1.socket; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 import java.net.ServerSocket; 7 import java.net.Socket; 8 9 public class ServerSocketTest { 10 11 public static void main(String[] args) { 12 try { 13 //初始化服务端socket并且绑定9999端口 14 ServerSocket serverSocket = new ServerSocket(9999); 15 //等待客户端的连接 16 Socket socket = serverSocket.accept(); 17 //获取输入流 18 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); 19 //读取一行数据 20 String str = bufferedReader.readLine(); 21 //输出打印 22 System.out.println(str); 23 } catch (IOException e) { 24 e.printStackTrace(); 25 } 26 } 27 }
客户端
1 package socket.socket1.socket; 2 3 import java.io.BufferedWriter; 4 import java.io.IOException; 5 import java.io.OutputStreamWriter; 6 import java.net.Socket; 7 8 public class ClientSocket { 9 public static void main(String[] args) { 10 try { 11 Socket socket = new Socket("127.0.0.1", 9999); 12 BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 13 String str = "你好,这是我的第一个socket"; 14 bufferedWriter.write(str); 15 } catch (IOException e) { 16 e.printStackTrace(); 17 } 18 } 19 }
先启动服务端
再启动客户端
发现客户端启动正常后,马上执行完后关闭。同时服务端控制台报错:
服务端报错
然后好多童鞋,就拷贝这个java.net.SocketException: Connection reset上王查异常,查询解决方案,搞了半天都不知道怎么回事。
解决这个问题我们首先要明白,socket通信是阻塞的,他会在以下几个地方进行阻塞。
第一个是accept方法,调用这个方法后,服务端一直阻塞在哪里,直到有客户端连接进来。
第二个是read方法,调用read方法也会进行阻塞。通过上面的示例我们可以发现,该问题发生在read方法中。
有朋友说是Client没有发送成功,其实不是的,我们可以通debug跟踪一下,发现客户端发送了,并且没有问题。
而是发生在服务端中,当服务端调用read方法后,他一直阻塞在哪里,因为客户端没有给他一个标识,告诉是否消息发送完成,
所以服务端还在一直等待接受客户端的数据,结果客户端此时已经关闭了,就是在服务端报错:java.net.SocketException: Connection reset
那么理解上面的原理后,我们就能明白,客户端发送完消息后,需要给服务端一个标识,告诉服务端,我已经发送完成了,服务端就可以将接受的消息打印出来。
通常大家会用以下方法进行进行结束:
调用socket.close() 或者socket.shutdownOutput()方法。
调用这俩个方法,都会结束客户端socket。但是有本质的区别。
socket.close() 将socket关闭连接,那边如果有服务端给客户端反馈信息,此时客户端是收不到的。
socket.shutdownOutput()是将输出流关闭,此时,如果服务端有信息返回,则客户端是可以正常接受的。
现在我们将上面的客户端示例修改一下啊,增加一个标识告诉流已经输出完毕:
客户端
1 package socket.socket1.socket; 2 3 import java.io.BufferedWriter; 4 import java.io.IOException; 5 import java.io.OutputStreamWriter; 6 import java.net.Socket; 7 8 public class ClientSocket { 9 public static void main(String[] args) { 10 try { 11 Socket socket = new Socket("127.0.0.1", 9999); 12 BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 13 String str = "你好,这是我的第一个socket"; 14 bufferedWriter.write(str); 15 //刷新输入流 16 bufferedWriter.flush(); 17 //关闭socket的输出流 18 socket.shutdownOutput(); 19 } catch (IOException e) { 20 e.printStackTrace(); 21 } 22 } 23 }
在看服务端控制台:
通过上面示例,我们可以基本了解socket通信原理,掌握了一些socket通信的基本api和方法,实际应用中,都是通过此处进行实现变通的。
但上面示例,其实不够完整,比如我们每次发送都要new 一个socket ,也只支持一次发送消息,所以我们用另外一个例子,实现1个比较完整的demo
三 手写完整示例
例用Socket实现客户端和服务端通信,要求客户发送数据后回显相同的数据
服务端 socket
package com.differ.jackyun.examples.javabasisc.socket; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 服务端soccket 测试 * * @author hup * @data 2020-05-31 14:30 **/ public class MyServerSocket implements Runnable { @Override public void run() { //创建一个线程池 ExecutorService executorService = Executors.newFixedThreadPool(100); try { ServerSocket server = new ServerSocket(10001); while(true) { //阻塞等待 Socket socket = server.accept(); //为了支持并发,所以每来1次消息,都弄个新线程处理 Runnable runnable = () -> { //字符输入流 BufferedReader reader = null; //字符输出流 PrintWriter pw = null; try { //读取接收到的内容 reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //接收到的数据 String readResult = reader.readLine(); System.out.println("服务端接收到数据=" + readResult); //数据发回客户端 pw = new PrintWriter(socket.getOutputStream(), true); pw.println(readResult); } catch (Exception e) { } finally { //关闭流 try { if (reader != null) { reader.close(); } if (pw != null) { pw.close(); } } catch (Exception e) { } } }; //线程池提交线程任务 executorService.submit(runnable); } } catch (Exception ex) { ex.printStackTrace(); } } }
客户端 socket
package com.differ.jackyun.examples.javabasisc.socket; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; /** * 客户端socket * * @author hup * @data 2020-05-31 14:30 **/ public class MySocket implements Runnable { @Override public void run() { //输出字符流 PrintWriter pw = null; //输入字符流 BufferedReader reader = null; try { //输出字符流 Socket socket = new Socket("localhost", 10001); pw = new PrintWriter(socket.getOutputStream(), true); //向服务端发送消息 pw.println("我是客户端消息,今天天气真好"); //等待服务器端的消息 reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); while (true) { String result = reader.readLine(); if (result != null) { System.out.println("客户端接收到服务端消息=" + result); break; } } } catch (Exception ex) { } finally { //关闭流 try { if (pw != null) { pw.close(); } if (reader != null) { reader.close(); } } catch (Exception e) { } } } }
测试类
1 package com.differ.jackyun.examples.javabasisc.socket; 2 3 import org.junit.Test; 4 5 /** 6 * 套接字测试类 7 * 8 * @author hup 9 * @data 2020-05-31 15:14 10 **/ 11 public class socketTest { 12 @Test 13 public void test() { 14 //启动服务端 15 MyServerSocket myServerSocket = new MyServerSocket(); 16 new Thread(myServerSocket).start(); 17 18 try { 19 Thread.currentThread().sleep(5000); 20 } catch (Exception ex) { 21 System.out.println(ex); 22 } 23 24 //启动客户端1 25 MySocket mySocket = new MySocket(); 26 new Thread(mySocket).start(); 27 28 //启动客户端2 29 MySocket mySocket2 = new MySocket(); 30 new Thread(mySocket2).start(); 31 32 try { 33 Thread.currentThread().sleep(10000); 34 } catch (Exception ex) { 35 System.out.println(ex); 36 } 38 } 40 }
测试输出结果
服务端接收到数据=我是客户端消息,今天天气真好
客户端接收到服务端消息=我是客户端消息,今天天气真好
服务端接收到数据=我是客户端消息,今天天气真好
客户端接收到服务端消息=我是客户端消息,今天天气真好
根据结果可以知道: 多个客户端给服务端发消息,服务端都能处理(用到了多线程)
四 看完上面例子,可能有同学有疑问了,为什么你输入流(读取)的时候用的是BufferedReader, 输出流(写)的时候用的是PrintWriter 不应该用与BufferedReader 配套的BufferedWriter吗?
Socket编程中,尽量用PrintWriter取代BufferedWriter,下面是PrintWriter的优点:
1. PrintWriter的print、println方法可以接受任意类型的参数,而BufferedWriter的write方法只能接受字符、字符数组和字符串;
2. PrintWriter的println方法自动添加换行,BufferedWriter需要显示调用newLine方法;
3. PrintWriter的方法不会抛异常,若关心异常,需要调用checkError方法看是否有异常发生;
4. PrintWriter构造方法可指定参数,实现自动刷新缓存(autoflush);
5. PrintWriter的构造方法更广。
在使用BufferedReader中的readLine方法接收BufferedWriter中的字符流时,由于readLine是在读取到换行符的时候才将整行字符返回,所以BufferedWriter方法在录入一段字符后要使用newLine方法进行一次换行操作,然后再把字符流刷出去。而PrintWriter由于可以开启自动刷新,并且其中的println方法自带换行操作。所以代码实现起来要比BufferedWriter简单一些。
————————————————
版权声明:最后面这部分总结 来源于下面链接
原文链接:https://blog.csdn.net/arno_dzl/java/article/details/76601852