《疯狂java讲义》学习(48):TCP协议的网络编程

2024-04-17 20:48

本文主要是介绍《疯狂java讲义》学习(48):TCP协议的网络编程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1.基于TCP协议的网络编程

TCP/IP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket,从而在通信的两端之间形成网络虚拟链路。一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信。Java对基于TCP协议的网络通信提供了良好的封装,Java使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。

1.1 TCP协议基础

IP协议是Internet上使用的一个关键协议,它的全称是Internet Protocol,即Internet 协议,通常简称为IP协议。通过使用IP协议,从而使Internet成为一个允许连接不同类型的计算机和不同操作系统的网络。
要使两台计算机彼此能进行通信,必须使两台计算机使用同一种“语言”,IP协议只保证计算机能发送和接收分组数据。IP协议负责将消息从一个主机传送到另一个主机,消息在传送的过程中被分割成一个个的小包。
尽管计算机通过安装IP软件,保证了计算机之间可以发送和接收数据,但IP协议还不能解决数据分组在传输过程中可能出现的问题。因此,若要解决可能出现的问题,连上Internet的计算机还需要安装TCP协议来提供可靠并且无差错的通信服务。
TCP协议被称作一种端对端协议。这是因为它对两台计算机之间的连接起了重要作用——当一台计算机需要与另一台远程计算机连接时,TCP协议会让它们建立一个连接:用于发送和接收数据的虚拟链路。
TCP协议负责收集这些信息包,并将其按适当的次序放好传送,接收端收到后再将其正确地还原。TCP协议保证了数据包在传送中准确无误。TCP协议使用重发机制——当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体的确认信息,如果没有收到另一个通信实体的确认信息,则会再次重发刚才发送的信息。
通过这种重发机制,TCP协议向应用程序提供了可靠的通信连接,使它能够自动适应网上的各种变化。即使在Internet暂时出现堵塞的情况下,TCP也能够保证通信的可靠性。
综上所述,虽然IP和TCP这两个协议的功能不尽相同,也可以分开单独使用,但它们是在同一时期作为一个协议来设计的,并且在功能上也是互补的。只有两者结合起来,才能保证Internet在复杂的环境下正常运行。凡是要连接到Internet的计算机,都必须同时安装和使用这两个协议,因此在实际中常把这两个协议统称为TCP/IP协议。

1.2使用ServerSocket创建TCP服务器端

Java中能接收其他通信实体连接请求的类是ServerSocket,ServerSocket对象用于监听来自客户端的Socket连接,如果没有连接,它将一直处于等待状态。ServerSocket包含一个监听来自客户端连接请求的方法。

  • Socket accept():如果接收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket;否则该方法将一直处于等待状态,线程也被阻塞。

为了创建ServerSocket对象,ServerSocket类提供了如下几个构造器。

  • ServerSocket(int port):用指定的端口port来创建一个ServerSocket。该端口应该有一个有效的端口整数值,即0~65535。
  • ServerSocket(int port,int backlog):增加一个用来改变连接队列长度的参数backlog。
  • ServerSocket(int port,int backlog,InetAddress localAddr):在机器存在多个IP地址的情况下,允许通过localAddr参数来指定将ServerSocket绑定到指定的IP地址。

当ServerSocket使用完毕后,应使用ServerSocket的close()方法来关闭该ServerSocket。在通常情况下,服务器不应该只接收一个客户端请求,而应该不断地接收来自客户端的所有请求,所以Java程序通常会通过循环不断地调用ServerSocket的accept()方法:

// 创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket ss=new ServerSocket(30000);
//采用循环不断地接收来自客户端的请求
while (true)
{//每当接收到客户端Socket的请求时,服务器端也对应产生一个SocketSocket s=ss.accept();//下面就可以使用Socket进行通信了...
}

1.3 使用Socket进行通信

客户端通常可以使用Socket的构造器来连接到指定服务器,Socket通常可以使用如下两个构造器。

  • Socket(InetAddress/String remoteAddress, int port):创建连接到指定远程主机、远程端口的Socket,该构造器没有指定本地地址、本地端口,默认使用本地主机的默认IP地址,默认使用系统动态分配的端口。
  • Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort):创建连接到指定远程主机、远程端口的Socket,并指定本地IP地址和本地端口,适用于本地主机有多个IP地址的情形。

上面两个构造器中指定远程主机时既可使用InetAddress来指定,也可直接使用String对象来指定,但程序通常使用String对象(如192.168.2.23)来指定远程IP地址。当本地主机只有一个IP地址时,使用第一个方法更为简单。如下代码所示:

//创建连接到本机、30000端口的
SocketSocket s=new Socket("127.0.0.1" , 30000);
//下面就可以使用Socket进行通信了
...

当程序执行上面代码中的粗体字代码时,该代码将会连接到指定服务器,让服务器端的ServerSocket的accept()方法向下执行,于是服务器端和客户端就产生一对互相连接的Socket。
当客户端、服务器端产生了对应的Socket之后,就得到了如图17.4所示的通信示意图,程序无须再区分服务器端、客户端,而是通过各自的Socket进行通信。Socket提供了如下两个方法来获取输入流和输出流。

  • InputStream getInputStream():返回该Socket对象对应的输入流,让程序通过该输入流从Socket中取出数据。
  • OutputStream getOutputStream():返回该Socket对象对应的输出流,让程序通过该输出流向Socket中输出数据。

看到这两个方法返回的InputStream和OutputStream,读者应该可以明白Java在设计IO体系上的苦心了——不管底层的IO流是怎样的节点流:文件流也好,网络Socket产生的流也好,程序都可以将其包装成处理流,从而提供更多方便的处理。下面以一个最简单的网络通信程序为例来介绍基于TCP协议的网络通信。
下面的服务器端程序非常简单,它仅仅建立ServerSocket监听,并使用Socket获取输出流输出。

package SocketServer;import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;public class Server
{public static void main(String[] args) throws IOException{// 创建一个ServerSocket,用于监听客户端Socket的连接请求ServerSocket ss=new ServerSocket(30000);// 采用循环不断地接收来自客户端的请求while (true){// 每当接收到客户端Socket的请求时,服务器端也对应产生一个SocketSocket s=ss.accept();                // 将Socket对应的输出流包装成PrintStreamPrintStream ps=new PrintStream(s.getOutputStream());// 进行普通IO操作ps.println("您好,您收到了服务器的新年祝福!");// 关闭输出流,关闭Socketps.close();s.close();}}
}

下面的客户端程序也非常简单,它仅仅使用Socket建立与指定IP地址、指定端口的连接,并使用Socket获取输入流读取数据:

package SocketServer;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;public class Client {public static void main(String[] args) throws IOException {Socket socket=new Socket("127.0.0.1" , 30000); //①// 将Socket对应的输入流包装成BufferedReaderBufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));// 进行普通IO操作String line=br.readLine();System.out.println("来自服务器的数据:" + line);// 关闭输入流,关闭Socketbr.close();socket.close();}
}

先运行程序中的Server类,将看到服务器一直处于等待状态,因为服务器使用了死循环来接收来自客户端的请求;再运行Client类,将看到程序输出:“来自服务器的数据:您好,您收到了服务器的新年祝福!”,这表明客户端和服务器端通信成功。
在实际应用中,程序可能不想让执行网络连接、读取服务器数据的进程一直阻塞,而是希望当网络连接、读取操作超过合理时间之后,系统自动认为该操作失败,这个合理时间就是超时时长。Socket对象提供了一个setSoTimeout(int timeout)方法来设置超时时长。如下代码片段所示:

Socket s=new Socket("127.0.0.1" , 30000);
//设置10秒之后即认为超时
s.setSoTimeout(10000);

当我们为Socket对象指定了超时时长之后,如果在使用Socket进行读、写操作完成之前超出了该时间限制,那么这些方法就会抛出SocketTimeoutException异常,程序可以对该异常进行捕获,并进行适当处理。如下代码所示:

try
{// 使用Scanner来读取网络输入流中的数据Scanner scan=new Scanner(s.getInputStream())// 读取一行字符String line=scan.nextLine()...
}
// 捕获SocketTimeoutException异常
catch(SocketTimeoutException ex)
{// 对异常进行处理...
}

假设程序需要为Socket连接服务器时指定超时时长,即经过指定时间后,如果该Socket还未连接到远程服务器,则系统认为该Socket连接超时。但Socket的所有构造器里都没有提供指定超时时长的参数,所以程序应该先创建一个无连接的Socket,再调用Socket的connect()方法来连接远程服务器,而connect()方法就可以接收一个超时时长参数。如下代码所示:

//创建一个无连接的Socket
Socket s=new Socket();
//让该Socket连接到远程服务器,如果经过10秒还没有连接上,则认为连接超时
s.connect(new InetAddress(host, port) ,10000);

1.4 加入多线程

前面Server和Client只是进行了简单的通信操作:服务器端接收到客户端连接之后,服务器端向客户端输出一个字符串,而客户端也只是读取服务器端的字符串后就退出了。实际应用中的客户端则可能需要和服务器端保持长时间通信,即服务器端需要不断地读取客户端数据,并向客户端写入数据;客户端也需要不断地读取服务器端数据,并向服务器端写入数据。
当我们使用传统BufferedReader的readLine()方法读取数据时,在该方法成功返回之前,线程被阻塞,程序无法继续执行。考虑到这个原因,服务器端应该为每个Socket单独启动一个线程,每个线程负责与一个客户端进行通信。
客户端读取服务器端数据的线程同样会被阻塞,所以系统应该单独启动一个线程,该线程专门负责读取服务器端数据。
现在考虑实现一个命令行界面的C/S聊天室应用,服务器端应该包含多个线程,每个Socket对应一个线程,该线程负责读取Socket对应输入流的数据(从客户端发送过来的数据),并将读到的数据向每个Socket输出流发送一次(将一个客户端发送的数据“广播”给其他客户端),因此需要在服务器端使用List来保存所有的Socket。
下面是服务器端的实现代码,程序为服务器端提供了两个类,一个是创建ServerSocket监听的主类,一个是负责处理每个Socket通信的线程类:

package MultiThread.server;import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;public class MyServer {//定义保存所有Socket的ArrayListpublic static ArrayList<Socket> socketList = new ArrayList<>();public static void main(String[] args) throws IOException {ServerSocket ss=new ServerSocket(30000);while(true) {// 此行代码会阻塞,将一直等待别人的连接Socket s=ss.accept();socketList.add(s);// 每当客户端连接后启动一个ServerThread线程为该客户端服务new Thread(new ServerThread(s)).start();}}
}

上面程序实现了服务器端只负责接收客户端Socket的连接请求,每当客户端Socket连接到该ServerSocket之后,程序将对应Socket加入socketList集合中保存,并为该Socket启动一个线程,该线程负责处理该Socket所有的通信任务,如程序中四行粗体字代码所示。服务器端线程类的代码如下:

package MultiThread.server;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;// 负责处理每个线程通信的线程类
public class ServerThread implements Runnable {// 定义当前线程所处理的SocketSocket s=null;// 该线程所处理的Socket对应的输入流BufferedReader br=null;public ServerThread(Socket s) throws IOException {this.s=s;// 初始化该Socket对应的输入流br=new BufferedReader(new InputStreamReader(s.getInputStream()));}public void run() {try {String content=null;// 采用循环不断地从Socket中读取客户端发送过来的数据while ((content=readFromClient()) !=null) {// 遍历socketList中的每个Socket// 将读到的内容向每个Socket发送一次for (Socket s : MyServer.socketList){PrintStream ps=new PrintStream(s.getOutputStream());ps.println(content);}}}catch (IOException e) {e.printStackTrace();}}// 定义读取客户端数据的方法private String readFromClient() {try {return br.readLine();}// 如果捕获到异常,则表明该Socket对应的客户端已经关闭catch (IOException e) {// 删除该SocketMyServer.socketList.remove(s);     // ①}return null;}
}

上面的服务器端线程类不断地读取客户端数据,程序使用readFromClient()方法来读取客户端数据,如果读取数据过程中捕获到IOException异常,则表明该Socket对应的客户端Socket出现了问题(到底什么问题我们不管,反正不正常),程序就将该Socket从socketList集合中删除,如readFromClient()方法中①号代码所示。
当服务器端线程读到客户端数据之后,程序遍历socketList集合,并将该数据向socketList集合中的每个Socket发送一次——该服务器端线程把从Socket中读到的数据向socketList集合中的每个Socket转发一次,如run()线程执行体中的粗体字代码所示。
每个客户端应该包含两个线程,一个负责读取用户的键盘输入,并将用户输入的数据写入Socket对应的输出流中;一个负责读取Socket对应输入流中的数据(从服务器端发送过来的数据),并将这些数据打印输出。其中负责读取用户键盘输入的线程由MyClient负责,也就是由程序的主线程负责。客户端主程序代码如下:

package MultiThread.client;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;public class MyClient {public static void main(String[] args)throws Exception {Socket s;s = new Socket("127.0.0.1" , 30000);// 客户端启动ClientThread线程不断地读取来自服务器的数据new Thread(new ClientThread(s)).start();   //①// 获取该Socket对应的输出流PrintStream ps=new PrintStream(s.getOutputStream());String line=null;
// 不断地读取键盘输入BufferedReader br=new BufferedReader(new InputStreamReader(System.in));while ((line=br.readLine()) !=null) {// 将用户的键盘输入内容写入Socket对应的输出流ps.println(line);          }}
}

当该线程读到用户键盘输入的内容后,将用户键盘输入的内容写入该Socket对应的输出流。
除此之外,当主线程使用Socket连接到服务器之后,启动了ClientThread来处理该线程的Socket通信,如程序中①号代码所示。ClientThread线程负责读取Socket输入流中的内容,并将这些内容在控制台打印出来:

package MultiThread.client;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;public class ClientThread implements Runnable {// 该线程负责处理的Socketprivate Socket s;// 该线程所处理的Socket对应的输入流BufferedReader br=null;public ClientThread(Socket s) throws IOException {this.s=s;br=new BufferedReader(new 

这篇关于《疯狂java讲义》学习(48):TCP协议的网络编程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/912818

相关文章

Java反转字符串的五种方法总结

《Java反转字符串的五种方法总结》:本文主要介绍五种在Java中反转字符串的方法,包括使用StringBuilder的reverse()方法、字符数组、自定义StringBuilder方法、直接... 目录前言方法一:使用StringBuilder的reverse()方法方法二:使用字符数组方法三:使用自

JAVA封装多线程实现的方式及原理

《JAVA封装多线程实现的方式及原理》:本文主要介绍Java中封装多线程的原理和常见方式,通过封装可以简化多线程的使用,提高安全性,并增强代码的可维护性和可扩展性,需要的朋友可以参考下... 目录前言一、封装的目标二、常见的封装方式及原理总结前言在 Java 中,封装多线程的原理主要围绕着将多线程相关的操

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx

Spring Cloud之注册中心Nacos的使用详解

《SpringCloud之注册中心Nacos的使用详解》本文介绍SpringCloudAlibaba中的Nacos组件,对比了Nacos与Eureka的区别,展示了如何在项目中引入SpringClo... 目录Naacos服务注册/服务发现引⼊Spring Cloud Alibaba依赖引入Naco编程s依

java导出pdf文件的详细实现方法

《java导出pdf文件的详细实现方法》:本文主要介绍java导出pdf文件的详细实现方法,包括制作模板、获取中文字体文件、实现后端服务以及前端发起请求并生成下载链接,需要的朋友可以参考下... 目录使用注意点包含内容1、制作pdf模板2、获取pdf导出中文需要的文件3、实现4、前端发起请求并生成下载链接使

Java springBoot初步使用websocket的代码示例

《JavaspringBoot初步使用websocket的代码示例》:本文主要介绍JavaspringBoot初步使用websocket的相关资料,WebSocket是一种实现实时双向通信的协... 目录一、什么是websocket二、依赖坐标地址1.springBoot父级依赖2.springBoot依赖

如何用java对接微信小程序下单后的发货接口

《如何用java对接微信小程序下单后的发货接口》:本文主要介绍在微信小程序后台实现发货通知的步骤,包括获取Access_token、使用RestTemplate调用发货接口、处理AccessTok... 目录配置参数 调用代码获取Access_token调用发货的接口类注意点总结配置参数 首先需要获取Ac

Java逻辑运算符之&&、|| 与&、 |的区别及应用

《Java逻辑运算符之&&、||与&、|的区别及应用》:本文主要介绍Java逻辑运算符之&&、||与&、|的区别及应用的相关资料,分别是&&、||与&、|,并探讨了它们在不同应用场景中... 目录前言一、基本概念与运算符介绍二、短路与与非短路与:&& 与 & 的区别1. &&:短路与(AND)2. &:非短

Java的volatile和sychronized底层实现原理解析

《Java的volatile和sychronized底层实现原理解析》文章详细介绍了Java中的synchronized和volatile关键字的底层实现原理,包括字节码层面、JVM层面的实现细节,以... 目录1. 概览2. Synchronized2.1 字节码层面2.2 JVM层面2.2.1 ente

什么是 Java 的 CyclicBarrier(代码示例)

《什么是Java的CyclicBarrier(代码示例)》CyclicBarrier是多线程协同的利器,适合需要多次同步的场景,本文通过代码示例讲解什么是Java的CyclicBarrier,感... 你的回答(口语化,面试场景)面试官:什么是 Java 的 CyclicBarrier?你:好的,我来举个例