一、什么是 Socket
百科:
白话就是,基于网络通信协议所制定的交互接口(接口就是规范)。
既然是接口,那必然就有实现,所以本次所有讲述的就是用 Java 来实现的一种 Socket 通信。但实现之前还是要铺垫一下两个网络通信中的重要协议:TCP/IP、UDP。
二、网络通信协议
在计算机中,两台设备要进行通信那就必须是在相同的协议中才能正常通信,而网络中经常本人说的两个协议就是 TCP、UDP。
2.1 面向连接的 TCP
“面向连接”就是在正式通信前必须要与对方建立起连接,是按照电话系统建模的。比如你给别人打电话,必须等线路接通了、对方拿起话筒才能相互通话。
TCP 协议是一种可靠的、一对一的、面向有连接的通信协议,TCP主要通过下列几种方式保证数据传输的可靠性:
在使用TCP协议进行数据传输时,往往需要客户端和服务端先建立一个“通道“、且这个通道只能够被客户端和服务端使用,所以TCP传输协议只能面向一对一的连接。
为了保证数据传输的准确无误,TCP传输协议将用于传输的数据包分为若干个部分(每个部分的大小根据当时的网络情况而定),然后在它们的首部添加一个检验字节。当数据的一个部分被接收完毕之后,服务端会对这一部分的完整性和准确性进行校验,校验之后如果数据的完整度和准确度都为100%,在服务端会要求客户端开始数据下一个部分的传输,如果数据的完整性和准确性与原来不相符,那么服务端会要求客户端再次传输这个部分。
客户端与服务端在使用TCP传输协议时要先建立一个“通道”,在传输完毕之后又要关闭这“通道”,前者可以被形象地成为“三次握手”,而后者则可以被称为“四次挥手”。
TCP 协议能为应用程序提供可靠的通信连接,使一台计算机发出的字节流无差错地发往网络上的其他计算机,对可靠性要求高的数据通信系统数据通信系统往往使用 TCP 协议传输数据。
2.2 无连接的 UDP 协议
“无连接”就是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。与手机短信非常相似:你在发短信的时候,只需要输入对方手机号就OK了。
UDP 传输协议是一种不可靠的、面向无连接、可以实现多对一、一对多和一对一连接的通信协议。UDP在传输数据前既不需要建立通道,在数据传输完毕后也不需要将通道关闭。只要客户端给服务端发送一个请求,服务端就会一次性地把所有数据发送完毕。UDP在传输数据时不会对数据的完整性进行验证,在数据丢失或数据出错时也不会要求重新传输,因此也节省了很多用于验证数据包的时间,所以以UDP建立的连接的延迟会比以TCP建立的连接的延迟更低。UDP不会根据当前的网络情况来控制数据的发送速度,因此无论网络情况是好是坏,服务端都会以恒定的速率发送数据。虽然这样有时会造成数据的丢失与损坏,但是这一点对于一些实时应用来说是十分重要的。基于以上三点,UDP在数据传输方面速度更快,延迟更低,实时性更好,因此被广泛地用于通信领域和视频网站当中。
UDP 适用于一次只传送少量数据、对可靠性要求不高的应用环境。比如,我们经常使用“ping”命令来测试两台主机之间 TCP/IP 通信是否正常,其实 “ping” 命令的原理就是向对方主机发送 ICMP 数据包,然后对方主机确认收到数据包,如果数据包是否到达的消息及时反馈回来,那么网络就是通的。
例如,在默认状态下,一次 “ping” 操作发送4个数据包。大家可以看到,发送的数据包数量是4包,收到的也是4包(因为对方主机收到后会发回一个确认收到的数据包)。这充分说明了UDP协议是面向非连接的协议,没有建立连接的过程。正因为UDP协议没有连接的过程,所以它的通信效率高;但也正因为如此,它的可靠性不如TCP协议高。QQ就使用UDP发消息,因此有时会出现收不到消息的情况。
2.3 两者区别
TCP/IP 和UDP最大的区别就是:TCP/IP是面向连接的,UDP是无连接的。TCP协议和UDP协议各有所长、各有所短,适用于不同要求的通信环境。TCP协议和UDP协议之间的差别如下表所示。
在实际的使用中,TCP主要应用于文件传输精确性相对要求较高且不是很紧急的情景,比如电子邮件、远程登录等。有时在这些应用场景下即使丢失一两个字节也会造成不可挽回的错误,所以这些场景中一般都使用TCP传输协议。由于UDP可以提高传输效率,所以UDP被广泛应用于数据量大且精确性要求不高的数据传输,比如我们平常在网站上观看视频或者听音乐的时候应用的基本上都是UDP传输协议。
对比图:
三、基于 TCP/IP 的 Socket 通信
简单需求:实现客户端与服务端之间基本通信,客户端发一条消息服务端显示客户端消息并回复客户端。
先写服务 code
public class TcpSocketServer { /** * 服务端程序 */ public void server() throws IOException { Scanner scanner = new Scanner(System.in); // 服务端监听 9528 端口 ServerSocket serverSocket = new ServerSocket(9528); System.out.println("等待连接"); Socket client = serverSocket.accept(); System.out.println("连接成功!"); while (true) { // 获取客户端输入流 InputStream inputStream = client.getInputStream(); byte[] bytes = new byte[1024]; int read = inputStream.read(bytes); // 客户端发来的消息 System.out.println("客户端:" + new String(bytes, 0, read, Charset.defaultCharset())); // 给客户端发端东西 System.out.print("请输入:"); String nextLine = scanner.next(); if ("out".equals(nextLine)) { break; } client.getOutputStream().write(nextLine.getBytes(StandardCharsets.UTF_8)); } } public static void main(String[] args) throws IOException { TcpSocketServer tcpSocketServer = new TcpSocketServer(); tcpSocketServer.server();; } }
再写客户端 code
public class TcpSocketClient { /** * 客户端程序 */ public void client() throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("等待连接服务端!"); Socket socket = new Socket("127.0.0.1", 9528); System.out.println("连接服务端成功!"); while (true) { // 给服务端发点东西 System.out.print("请输入:"); String s = scanner.next(); if ("out".equals(s)) { break; } OutputStream outputStream = socket.getOutputStream(); outputStream.write(s.getBytes(StandardCharsets.UTF_8)); byte[] bytes = new byte[1024]; // 读一下服务端发来的东西 InputStream inputStream = socket.getInputStream(); int read = inputStream.read(bytes); System.out.println("服务端:" + new String(bytes, 0, read, Charset.defaultCharset())); } } public static void main(String[] args) throws IOException { TcpSocketClient tcpSocketServer = new TcpSocketClient(); tcpSocketServer.client(); } }
效果
四、基于 UDP 的 Socket 通信
同理,用UDP的方式实现一下上面的需求
服务端code
public class UdpSocketServer { /** * 服务端程序 */ public void server() throws IOException { Scanner scanner = new Scanner(System.in); // 服务端监听 9528 端口 DatagramSocket serverSocket = new DatagramSocket(9528); while (true) { // 获取客户端输入流 byte[] bytes = new byte[1024]; DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length); System.out.println("****服务器端已经启动,等待客户端发送数据"); serverSocket.receive(datagramPacket); // 客户端发来的消息 System.out.println("客户端:" + new String(datagramPacket.getData(), 0, datagramPacket.getLength(), Charset.defaultCharset())); // 给客户端发端东西 System.out.print("请输入:"); String nextLine = scanner.next(); if ("out".equals(nextLine)) { break; } byte[] bytes1 = nextLine.getBytes(StandardCharsets.UTF_8); DatagramPacket datagramPacket1 = new DatagramPacket(bytes1, bytes1.length, datagramPacket.getAddress(), datagramPacket.getPort()); serverSocket.send(datagramPacket1); } } public static void main(String[] args) throws IOException { UdpSocketServer tcpSocketServer = new UdpSocketServer(); tcpSocketServer.server(); ; } }
客户端 code
public class UdpSocketClient { /** * 客户端程序 */ public void client() throws IOException { Scanner scanner = new Scanner(System.in); while (true) { DatagramSocket datagramSocket = new DatagramSocket(); datagramSocket.connect(new InetSocketAddress("127.0.0.1", 9528)); String next = scanner.next(); if ("out".equals(next)) { break; } byte[] bytes = next.getBytes(StandardCharsets.UTF_8); DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length); datagramSocket.send(datagramPacket); // 获取客户端输入流 byte[] bytes1 = new byte[1024]; DatagramPacket datagramPacket1 = new DatagramPacket(bytes1, bytes1.length); datagramSocket.receive(datagramPacket1); // 客户端发来的消息 System.out.println("服务端:" + new String(datagramPacket1.getData(), 0, datagramPacket1.getLength(), Charset.defaultCharset())); } } public static void main(String[] args) throws IOException { UdpSocketClient tcpSocketServer = new UdpSocketClient(); tcpSocketServer.client(); } }
效果:
上述代码如果严谨点,应该在退出的时候关闭流操作,但自己知道就行这也是为了代码简便省略了多余的关闭流操作而多写很多判断代码,但实际开发的时候注意这一点就行了。
五、最后
对于 Socekt 编程,也即网络编程,我们可以看到不论是来连接,或者读取数据都是阻塞的要等待一定时间才能继续往下执行。
如果在主线程中这样做的话,那对于程序而言是非常糟糕的一件事情而且用 Java 所以共的原生 API 去开发一个网络程序是非常繁琐的,当然精通 Socket 的高级工程师除外。所以 NIO 中就对 Socket 编程做了进一步的封装提供了更好的编程方法来提升程序效率,减小开发成本,下篇我们就来说它。
好了,今天的内容到这里就结束了,关注我,我们下期见