《疯狂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

相关文章

51单片机学习记录———定时器

文章目录 前言一、定时器介绍二、STC89C52定时器资源三、定时器框图四、定时器模式五、定时器相关寄存器六、定时器练习 前言 一个学习嵌入式的小白~ 有问题评论区或私信指出~ 提示:以下是本篇文章正文内容,下面案例可供参考 一、定时器介绍 定时器介绍:51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。 定时器作用: 1.用于计数系统,可

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

[word] word设置上标快捷键 #学习方法#其他#媒体

word设置上标快捷键 办公中,少不了使用word,这个是大家必备的软件,今天给大家分享word设置上标快捷键,希望在办公中能帮到您! 1、添加上标 在录入一些公式,或者是化学产品时,需要添加上标内容,按下快捷键Ctrl+shift++就能将需要的内容设置为上标符号。 word设置上标快捷键的方法就是以上内容了,需要的小伙伴都可以试一试呢!

AssetBundle学习笔记

AssetBundle是unity自定义的资源格式,通过调用引擎的资源打包接口对资源进行打包成.assetbundle格式的资源包。本文介绍了AssetBundle的生成,使用,加载,卸载以及Unity资源更新的一个基本步骤。 目录 1.定义: 2.AssetBundle的生成: 1)设置AssetBundle包的属性——通过编辑器界面 补充:分组策略 2)调用引擎接口API

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

大学湖北中医药大学法医学试题及答案,分享几个实用搜题和学习工具 #微信#学习方法#职场发展

今天分享拥有拍照搜题、文字搜题、语音搜题、多重搜题等搜题模式,可以快速查找问题解析,加深对题目答案的理解。 1.快练题 这是一个网站 找题的网站海量题库,在线搜题,快速刷题~为您提供百万优质题库,直接搜索题库名称,支持多种刷题模式:顺序练习、语音听题、本地搜题、顺序阅读、模拟考试、组卷考试、赶快下载吧! 2.彩虹搜题 这是个老公众号了 支持手写输入,截图搜题,详细步骤,解题必备

【Altium】查找PCB上未连接的网络

【更多软件使用问题请点击亿道电子官方网站】 1、文档目标: PCB设计后期检查中找出没有连接的网络 应用场景:PCB设计后期,需要检查是否所有网络都已连接布线。虽然未连接的网络会有飞线显示,但是由于布线后期整板布线密度较高,虚连,断连的网络用肉眼难以轻易发现。用DRC检查也可以找出未连接的网络,如果PCB中DRC问题较多,查找起来就不是很方便。使用PCB Filter面板来达成目的相比DRC