使用JAVA网络技术实践
【摘要】 学习目标²网络协议的简单原理掌握²Socket与ServerSocket的原理及使用²实现基于TCP协议传送的简单服务器、客户端应用²实现基于UDP协议传送的简单服务器、客户端应用²InetAddress类的方法掌握及使用Java技术的组成TCP/IP协议模型计算机网络的组成部分:计算机系统、数据通信系统、网络软件。其中,网络软件充当网络管理者的角色,它提供并实现各种网络服务,包括网络协议软...
学习目标
- ²网络协议的简单原理掌握
- ²Socket与ServerSocket的原理及使用
- ²实现基于TCP协议传送的简单服务器、客户端应用
- ²实现基于UDP协议传送的简单服务器、客户端应用
- ²InetAddress类的方法掌握及使用Java技术的组成
TCP/IP协议模型
计算机网络的组成部分:计算机系统、数据通信系统、网络软件。其中,网络软件充当网络管理者的角色,它提供并实现各种网络服务,包括网络协议软件(TCP/IP、IPX等协议驱动)、网络服务软件、网络工具软件、网络操作系统(Netware、NT局域网系统)。
网络通信协议
网络协议是构成网络的基本组件之一,协议是若干规则和协定的组合,一般指机器1的第n层与机器2的第n层的对话,这种对话中所使用的若干规则和约束便称为第n层网络协议。TCP/IP网络体系结构模型就是遵循TCP/IP协议进行通信的一种分层体系,现今,Internet和Intranet所使用的协议一般都为TCP/IP协议。
TCP/IP协议是一个工业标准协议套件,专为跨大广域网(WAN)的大型互联网络而设计。在了解该协议之前,我们必须掌握基于该协议的体系结构层次,而TCP/IP体系结构分为四层,具体结构如下图:
可以看出,TCP/IP体系模型分为4层结构,其中有3层对应于ISO参考模型中的相应层。这4层概述如下:
第一层 网络接口层
包括用于协作IP数据在已有网络介质上传输的协议,提供TCP/IP协议的数据结构和实际物理硬件之间的接口。比如地址解析协议(Address Resolution Protocol, ARP )等。
第二层 网络层
对应于ISO模型的网络层,主要包含了IP、RIP等相关协议,负责数据的打包、寻址及路由。还包括网间控制报文协议(ICMP)来提供网络诊断信息。
第三层 传输层
对应于ISO的传输层,提供了两种端到端的通信服务,分别是TCP和UDP协议。
第四层 应用层
对应于ISO的应用层和表达层,提供了网络与应用之间的对话接口。包含了各种网络应用层协议,比如Http、FTP等应用协议。
TCP/IP体系模型相对于ISO模型的7层结构来说更简单更实用!现已成为因特网之间的标准协议模型。
TCP/IP网络体系主要包含两种协议:TCP/IP、UDP协议。其中,IP(Internet Protocol)协议是一种低级路由协议,该协议主要实现将传输数据分解成许多小数据包,接着通过网络将这些数据包传到一个指定地址,但是,请注意,IP协议并不会保证传输的数据包一定到达目的地,或者是数据包的完整性!
TCP(Thransfer Control Protocol)协议正好弥补了IP协议的不足,属于一种较高级的协议,它实现了数据包的有力捆绑,通过排序和重传来确保数据传输的可靠(即数据的准确传输以及完整性)。排序可以保证数据的读取是按照正确的格式进行,重传则保证了数据能够准确传送到目的地!
UDP协议与TCP协议类似,它们之间的区别在于TCP协议是面向连接的可靠数据传输协议,而UDP协议是面向数据报的不可靠数据传输协议;UDP协议可以要求数据传输的目的地可以没有连接甚至不存在,数据传输效率更快,但可靠性低,TCP正好相反。
注意,TCP与UDP协议均属于传输层协议,而IP协议属于网络层协议。
应用层各种协议提供了应用程序访问其他层的服务,并定义应用程序用于交换数据的协议。以下应用协议是广泛被使用的交换用户信息的协议:
- 超文本传输协议(HTTP): 用于传输组成万维网Web页面的文件,大部分Web项目都是基于该协议实现用户数据的传输。
- 文件传输协议(FTP): 交互式文件传输
- 简单邮件传输协议(SMTP): 用于传输邮件消息和连接
- 终端访问协议(Telnet): 远程登录到网络主机
- 域名系统(DNS)
- 路由选择信息协议(RIP)
通信端口
每一个应用协议都有其特定的端口,通过端口实现服务器同时能够服务于很多不同的客户端,服务器进程通过监听端口来捕获客户端连接。一个服务器允许在同个端口接受多个客户,一个服务器也能开启多个端口接受客户请求。能够接受并管理多个客户连接的服务器进程必须是支持多线程的(或者采用同步输入/输出处理多路复用技术的其他方法)。
每一个端口都有特定的端口号,TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。同时该号一般对应一些特定协议,TCP/IP为特定协议保留了低端的1024个端口,其中,端口80是HTTP协议的,21是FTP协议的,23是Telnet协议的等等,客户和服务器必须事先约定所使用的端口。如果系统两部分所使用的端口不一致,那就不能进行通信。
HTTP是网络浏览器及服务器用来传输超文本网页和图像的协议,一般的Web服务器也提供了HTTP服务器的功能,当客户端向HTTP服务器请求一个资源时,HTTP协议会将相关数据以特定格式传给其缺省端口80,服务器在该端口接受请求并将处理结果返回给客户。可以这样理解,端口是基于某种特定协议的一个点(一般是服务器)与另一个点之间的对话窗口,在通话的过程中两点必须在同一窗口才能实现数据对话。
基于Java的网络技术
TCP/IP套接字
套接字是网络软件中的一个抽象概念,套接字允许单个计算机同时服务于很多不同客户,并能够提供不同类型信息的服务。该技术由引入的端口处理,该端口既是一个特定机器上的一个被编号的套接字---通信端口.TCP/IP套接字用于在主机和Internet之间建立的可靠、双向、点对点、持续的流式连接。
在java中,TCP/IP Socket连接是用java.net包中的类实现的,这些类实现了建立网络连接和通过连接发送数据的复杂过程,我们只需使用其简单接口就能实现网络通信!在java中有两类套接字,一种是服务器端套接字--java.net.ServerSocket,另一种是客户端套接字--java.net.Socket。
serverSocket
其中,ServerSocket被设计成在等待客户建立连接之前不做任何事情的监听器,构造方法的版本如下:
public ServerSocekt(int port) throws IOException
--在服务器指定端口port创建队列长度为50的服务器套接字,当port为0则代表创建一个基于任意可用端口的服务器套接字。队列长度告诉系统多少与之连接的客户在系统拒绝连接之前可以挂起。
public ServerSocekt(int port, int maxQueue) throws IOException
--在指定端口创建指定队列长度的服务器套接字
public ServerSocket(int port, int maxQueue, InetAddress bindAddr ) throws IOException
在多地址主机上,我们除了可以指定端口之外,还可以通过InetAddress类来指定该套接字约束的IP地址。InetAddress在后面将学习。
ServerSocket还定义了以下一些常用的方法:
public Socket accept() throws IOException
--该方法用于告诉服务器不停地等待,直到有客户端连接到该ServerSocket指定的端口,一旦有客户端通过网络向该端口发送正确的连接请求,该方法就会返回一个表示服务器与客户端连接已建立的Socket对象,接下来我们就可以通过这个返回的Socket对象实现服务器与指定客户端的通信。注意:accept()方法会让服务器中的当前线程暂停下来,直到有客户端的正确连接发送过来。
public void bind(SocketAddress endpoint) throws IOException
--绑定该ServerSocket到指定的endpoint地址(IP地址和端口)
public void close() throws IOException
--关闭当前ServerSocket。任何当前被锁定的线程将在accept()方法中抛出IOException。
从jdk1.4开始,java提供了关于ServerSocket的ServerSocketChannel,jdk建议用管道来实现客户端连接的监听以及关闭服务器套接字会更安全,因此,现在我们应该通过ServerSocket来得到其套接字管道,通过管道来实现服务监听以及关闭,可以通过ServerSocket的getChannel()方法来得到当前ServerSocket的相关管道。
Socket
该类为建立连向服务器套接字及启动协议交换而设计,当进程通过网络进行通信的时候,java技术使用流模型来实现数据的通信。一个Socket包括两个流,分别为一个输入流和一个输出流,一个进程如果要通过网络向另一个进程发送数据,只需简单地写入与Socket相关联的输出流,同样,一个进程通过从与Socket相关联的输入流来读取另一个进程所写的数据。如果通过TCP/IP协议建立连接,则服务器必须运行一个单独的进程来等待连接,而某一客户机必须试图到达服务器,就好比某人打电话,必须保证另一方等待电话呼叫,这样才能实现两人之间的通信。
分析以下代码:
import java.io.*;
import java.net.*;
try
{
// 在服务器的8000端口创建一个ServerSocket
ServerSocket server = new ServerSocket(8000);
// accept()方法使当前服务器线程暂停,之前创建的server套接字将通过该方法不停的监听指定端口是否有客户端请求连接发送过来,如果有正确连接发送过来,
// 则该方法返回一个表示连接已建立的Socket对象
Scoket fromSocket = server.accept();
if (fromSocket != null)
{
// 通过套接字实现数据传输
// 得到套接字的输入流
InputStream input = fromSocket.getInputStream();
// 得到套接字的输出流
OutputStream out = fromSocket.getOutputStream();
// 通过得到的输入、输出流实现数据的读取
}
}
catch(IOException e)
{
e.printStackTrace();
}
通过上面的代码,我们分析得知,套接字只是实现数据传输的接口,真正实现数据传输的是封装在套接字中的输入、输出流。以上代码是服务器端程序,该程序开启了一个端口号为8000的特定端口,并且开启了服务器套接字在该端口上监听客户请求,accept()方法阻塞当前线程直到有客户端请求发送过来,当请求正确时,该方法将客户端套接字引用传递出来,这样,网络之间的数据发送就好像是本地数据调用。
Socket类的相关方法:
Socket()创建一个无参数套接字,该套接字会随即取一个可用端口、可用IP来建立连接。
Socket(String host, int port)创建一个指定远程服务器、端口号的套接字
Socket(InetAddress net, int port)创建一个指定InetAddress类封装IP、指定端口号的套接字。即该套接字只能往指定IP的服务器以及服务器指定端口发送数据。
public OutputStream getOutputStream() throws IOException 得到套接字的输出流,接下来就可以使用得到的输出流去写数据至服务器或客户端
public InputStream getInputStream() throws IOException得到套接字的输入流,接下来就可以使用得到的输入流去读取来自于服务器或客户端的数据。
public SocketChannel getChannel()得到套接字的管道,在1.4之后新增了io功能,在java.nio包中定义,通过流中的SocketChannel来实现数据的读取会比直接使用流来读取更安全可靠。(扩展内容)
public void close() throws IOException关闭当前套接字,注意,关闭套接字的同时也会关闭该套接字中的所有流。
下面是客户端的部分代码:
import java.io.*;
import java.net.*;
try
{
// 创建套接字,该套接字连接IP地址为192.168.0.2的服务器,并向端口号为8000的端口连接
Socket server = new Socket("192.168.0.2",8000);
if (server != null)
{
// 通过套接字实现数据传输
// 得到套接字的输入流
InputStream input = fromSocket.getInputStream();
// 得到套接字的输出流
OutputStream out = fromSocket.getOutputStream();
// 通过得到的输入、输出流实现数据的读取
}
} catch(IOException e)
{
e.printStackTrace();
}
以上即是简单的基于Tcp/IP协议、使用套接字实现数据传输的服务器和客户端代码(注意:在运行时,先执行服务器端代码,接着执行客户端代码),但是我们发现,通过以上代码,客户端和服务器端只能进行一次数据对话,通过更改以上代码,我们才能实现一个客户端与服务器端的真正对话,直到某一方终止通话为止,修改后代码如下:
服务器端:
.....
ServerSocket server = new ServerSocket(8000);
while (true) {
// accept()方法使当前服务器线程暂停,之前创建的server套接字将通过该方法不停的监听指定端口是否有客户端请求连接发送过来,如果有正确连接发送过来,
// 则该方法返回一个表示连接已建立的Socket对象
Scoket fromSocket = server.accept();
if (fromSocket != null)
{
// 通过套接字实现数据传输
// 得到套接字的输入流
InputStream input = fromSocket.getInputStream();
// 得到套接字的输出流
OutputStream out = fromSocket.getOutputStream();
// 通过得到的输入、输出流实现数据的读取
.....
}
}
.....
客户端:
......
Socket server = new Socket("192.168.0.2",8000);
InputStream input = fromSocket.getInputStream();
OutputStream out = fromSocket.getOutputStream();
while (true)
{
// 数据的读写操作
......
}
......
实际上,我们只要将服务器端每一个客户的请求套接字的获取以及流的获取、数据的读写放入至一个无限循环,这样就可以保证服务器可以随时接受任意客户发送过来的套接字,从而实现对话。客户端也可以通过一个无限循环实现对服务器端的任意时刻的数据传输。
当然,这只是实现了一个客户和服务器之间的对话,但是服务器一般是对应许多客户的,因此,为了实现服务器对应多个客户,必须将每一次accept()方法返回的每一个客户套接字保存起来,这样才可以实现服务器与多个客户对应,具体代码在后面讨论。
注意,数据的读写应该是在一个独立于主线程的单独线程中运行,因为accept方法会阻塞当前线程,为了不影响主线程的其余功能,我们应该启用多线程来实现高效的数据传输。
当通过代码实现服务器与客户之间的套接字发送之后,我们就可以使用流的IO操作来实现服务器与客户端之间的基于特定协议的远程数据传输。注意,基于TCP/IP协议的数据传输,前提是必须得保证接受数据的一方是连通的,也就是说,如果服务器端没有运行,那么客户端是无法对其发送信息,反之亦然。那有时候我们只在乎信息的发送,并不理会接受的人是否连通,甚至不理会接受人是否存在,那就不能使用TCP/IP协议,而得通过我们下面所学的UDP协议来实现。
UDP套接字
UDP(User Datagrams Protocol)用户数据报协议,是一种使用数据报的机制来传递信息的协议。数据报(Datagrams)是一种在不同机器之间传递的信息包,该信息包一旦从某一机器被发送给指定目标,那么该发送过程并不会保证数据一定到达目的地,甚至不保证目的地的存在真实性。反之,数据报被接受时,不保证数据没有受损,也不保证发送该数据报的机器仍在等待响应。
由此可见,UDP协议是一种基于数据报的快速的(因为它无需花时间去保证数据是否损坏,无需花时间确定接受方是否存在并等待响应)、无连接的、不可靠的信息包传输协议。
在java中,通过两个特定类来实现UDP协议顶层数据报,分别是DatagramPacket和DatagramSocket,其中DatagramPacket是一个数据容器,用来保存即将要传输的数据;而DatagramSocket实现了发送和接收DatagramPacket的机制,即实现了数据报的通信方式。 1 、atagramPacket
该类主要有四个常用构造方法,分别如下:
DatagramPacket(byte[] buff, int length)
DatagramPacket(byte[] buf, int offset, int length)
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
第一个构造方法用于创建一个指定数据缓冲区大小和信息包的容量大小的DatagramPacket,第二个构造方法用于创建一个长度大小为length的缓冲区,并指定数据存储(读取)的偏移地址为offset的DatagramPacket。第三个创建一个指定缓冲区大小、传送(接受)IP地址、端口号的DatagramPacket。一般情况下,发送地址是由DatagramSocket指定。
常用的几个方法如下:
byte[] getData()
用于得到发送过来的DatagramPacket中的数据
void setData(byte[] buf)
用于将buf中的数据写入DatagramPacket中,以备发送。
InetAddress getAddress()
返回目标的InetAddress,一般用于发送。
DatagramSocket
DatagramSocket()
创建一个以当前计算机的任意可用端口为发送端口的数据报连接
DatagramSocket(int port)
创建一个以当前计算机的port端口为发送端口的数据报连接
DatagramScoket(int port, InetAddress address)
创建一个以当前计算机的port端口为发送端口,向IP地址为address的计算机发送数据报连接
常用的几个方法:
void close() throws IOException
关闭数据报连接
void recieve(DatagramPacket packet)
接收来自于packet数据报的信息
void send(DatagramPacket packet)
发送packet数据报
void connect(InetAddress address, int port)
以当前计算机指定端口port为发送端口,向IP地址为address的计算机发送数据报连接
void disconnect()
断开连接
DatagramChannel getChannel()
和SocketChannel类似
结合以上两个类的方法,创建一个简单的UDP服务器端如下:
....
// 指定连接端口
DatagramSocket socket = new DatagramSocket(8001)
byte[] buff = new byte[256];
// 指定接受数据报缓冲区大小为字节数组buff大小
DatagramPacket fromPacket = new DatagramPacket(buff,buf.length);
// 接受客户端请求,并将请求数据存储在数据报packet中
packet.recieve(fromPacket);
// 读取数据报中数据
byte rec = packet.getData();
.....
buff = "hello world".toBytes();
InetAddress addr = .....
// 指定发送数据报缓冲区大小为字节数组buff大小,发送目的地为addr,发送端口为8001,数据报内容为数组buff内容
DatagramPacket toPacket = new DatagramPacket(buff,buf.length,addr,8001);
// 发送服务器端数据报toPacket
packet.send(toPacket);
.......
客户端代码与服务器段类似,只要注意接受与发送应该分别是两个不同的数据报。
InetAddress类的使用
java.net.InetAddress类是java的IP地址封装类,内部隐藏了IP地址,可以通过它很容易的使用主机名以及IP地址。一般共各种网络类使用。直接由Object类派生并实现了序列化接口。该类用两个字段表示一个地址:hostName与address。hostName包含主机名,address包含IP地址。InetAddress支持ipv4与ipv6地址。
一些常用方法如下:
byte[] getAddress()
返回指定对象的IP地址的以网络字节为顺序的4个元素的字节数组
static InetAddress getByName(String hostname)
使用DNS查找hostname的IP地址,并返回
static InetAddress getLocalHost()
返回本地计算机的InetAddress。
String getHostName()
返回指定InetAddress对象的主机名。
String getHostAddress()
返回指定InetAddress对象的主机地址的字符串形式
分析:
InetAddress addr = InetAddress.getByName("java.sun.com");
System.out.println(addr);
以上代码将打印网址域名为java.sun.com的对应IP地址
因此,在网络编程中,我们可以很方便的使用InetAddress类实现Ip地址的各种操作。
例题1:服务器端:
package com.itjob.net;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.border.*;
import java.net.*;
public class ServerApp extends JFrame implements Runnable, ActionListener {
JPanel mainPanel;
JPanel bottomPanel;
GridBagLayout gbl;
GridBagConstraints gbc;
Border border;
JTextArea txtChatMess;
JScrollPane scroll;
JTextField txtMess;
JLabel lblEnterMess;
JButton cmdSend;
JButton cmdReset;
ServerSocket server = null;
Socket socket = null;
Scanner read = null;
public ServerApp() {
super("服务器");
mainPanel = new JPanel();
bottomPanel = new JPanel();
mainPanel.setLayout(new BorderLayout());
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
gbl = new GridBagLayout();
gbc = new GridBagConstraints();
bottomPanel.setLayout(gbl);
txtChatMess = new JTextArea(15, 20);
txtChatMess.setEditable(false);
scroll = new JScrollPane(txtChatMess);
mainPanel.add(scroll);
border = BorderFactory.createRaisedBevelBorder();
txtMess = new JTextField(15);
lblEnterMess = new JLabel("请输入消息: ");
cmdSend = new JButton("发 送");
cmdSend.setPreferredSize(new Dimension(50, 20));
cmdSend.setEnabled(false);
cmdSend.addActionListener(this);
cmdReset = new JButton("清 空");
cmdReset.setPreferredSize(new Dimension(50, 20));
cmdReset.addActionListener(this);
cmdSend.setBorder(border);
cmdReset.setBorder(border);
gbc.gridx = 3;
gbc.gridy = 10;
gbl.setConstraints(lblEnterMess, gbc);
bottomPanel.add(lblEnterMess);
gbc.gridx = 10;
//gbc.fill = gbc.BOTH;
gbl.setConstraints(txtMess, gbc);
bottomPanel.add(txtMess);
gbc.gridx = 3;
gbc.gridy = 30;
gbl.setConstraints(cmdSend, gbc);
bottomPanel.add(cmdSend);
gbc.gridx = 10;
gbl.setConstraints(cmdReset, gbc);
bottomPanel.add(cmdReset);
getContentPane().add(mainPanel);
pack();
setVisible(true);
}
public void actionPerformed(ActionEvent evt) {
if (evt.getSource() == cmdSend) {
try {
write.println(txtMess.getText());
write.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
if (evt.getSource() == cmdReset) {
txtMess.setText("");
}
}
public void run() {
try {
server = new ServerSocket(2005);
//反复接收客户端请求
while (true) {
socket = server.accept();
if (socket != null) {
txtChatMess.append("服务器消息:客户已连接!" + "\n");
cmdSend.setEnabled(true);
} else {
txtChatMess.append("服务器消息:客户未能连接!" + "\n");
cmdSend.setEnabled(false);
}
//为每一个客户启动一个读取数据的单独线程
Connections con = new Connections(socket);
Thread thread = new Thread(con);
thread.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
class Connections implements Runnable {
Socket clientSock = null;
Connections(Socket s) {
clientSock = s;
}
public void run()
{
try
{
read = new Scanner(socket.getInputStream()));
write = new PrintWriter(socket.getOutputStream());
String result = read.readLine();
if (result == null)
{
return;
}
while(!result.trim().equals("Exit!"))
{
txtChatMess.append("客户端消息: " + result + "\n");
if (read.hasNextLine())
{
esult = read.nextLine();
}
else
{
Thread.sleep(100);
}
}
}
catch(Exception e)
{
e.printStackTrace();
}finally{
try
{
read.close();
write.close();
socket.close();
server.close();
cmdSend.setEnabled(false);
}catch(Exception e)
{ ...
}
}
}
}
public static void main(String[] args) {
ServerApp serverApp = new ServerApp();
Thread thread = new Thread(serverApp);
thread.start();
}
}
客户端:
package com.itjob.net;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.border.*;
import java.net.*;
public class ClientApp extends JFrame implements Runnable, ActionListener {
JPanel mainPanel;
JPanel bottomPanel;
GridBagLayout gbl;
GridBagConstraints gbc;
Border border;
JTextArea txtChatMess;
JScrollPane scroll;
JTextField txtMess;
JLabel lblEnterMess;
JButton cmdSend;
JButton cmdReset;
ServerSocket server = null;
Socket socket = null;
Scanner read = null;
PrintWriter write = null;
public ClientApp() {
super("客户端");
mainPanel = new JPanel();
bottomPanel = new JPanel();
mainPanel.setLayout(new BorderLayout());
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
gbl = new GridBagLayout();
gbc = new GridBagConstraints();
bottomPanel.setLayout(gbl);
//new GBC();
txtChatMess = new JTextArea(15, 20);
txtChatMess.setEditable(false);
scroll = new JScrollPane(txtChatMess);
mainPanel.add(scroll);
border = BorderFactory.createRaisedBevelBorder();
txtMess = new JTextField(15);
lblEnterMess = new JLabel("请输入消息: ");
cmdSend = new JButton("发 送");
cmdSend.setPreferredSize(new Dimension(50, 20));
cmdSend.setEnabled(false);
cmdSend.addActionListener(this);
cmdReset = new JButton("清 空");
cmdReset.setPreferredSize(new Dimension(50, 20));
cmdReset.addActionListener(this);
cmdSend.setBorder(border);
cmdReset.setBorder(border);
gbc.gridx = 3;
gbc.gridy = 10;
gbl.setConstraints(lblEnterMess, gbc);
bottomPanel.add(lblEnterMess);
gbc.gridx = 10;
//gbc.fill = gbc.BOTH;
gbl.setConstraints(txtMess, gbc);
bottomPanel.add(txtMess);
gbc.gridx = 3;
gbc.gridy = 30;
gbl.setConstraints(cmdSend, gbc);
bottomPanel.add(cmdSend);
gbc.gridx = 10;
gbl.setConstraints(cmdReset, gbc);
bottomPanel.add(cmdReset);
getContentPane().add(mainPanel);
pack();
setVisible(true);
}
public void actionPerformed(ActionEvent evt) {
if (evt.getSource() == cmdSend) {
try {
write.println(txtMess.getText());
write.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
if (evt.getSource() == cmdReset) {
txtMess.setText("");
}
}
public void run() {
try {
InetAddress add = InetAddress.getLocalHost();
socket = new Socket(add, 2005);
read = new Scanner(socket.getInputStream());
write = new PrintWriter(socket.getOutputStream());
if (socket != null) {
txtChatMess.append("客户消息:服务器已连接!" + "\n");
cmdSend.setEnabled(true);
} else {
txtChatMess.append("客户消息:服务器未能连接!" + "\n");
cmdSend.setEnabled(false); }
} catch (Exception e) {
e.printStackTrace();
}
Connections con = new Connections();
Thread thread = new Thread(con);
thread.start();
}
class Connections implements Runnable {
public void run() {
try {
String result = read.readLine();
if (result == null) {
return;
}
while (!result.trim().equals("Exit!")) {
txtChatMess.append("服务器端消息: " + result + "\n");
if (read.hasNextLine()) {
result = read.nextLine();
} else {
Thread.sleep(100);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
read.close();
write.close();
socket.close();
server.close();
//cmdSend.setEnabled(false);
} catch (Exception e) {
}
}
}
}
public static void main(String[] args) {
ClientApp client = new ClientApp();
Thread thread = new Thread(client);
thread.start();
}
}
注意:以上代码是基于客户端与服务器在同一台服务器上的,而且服务器不能实现对应多个客户,请大家思考并修改,从而实现一个服务器对应多个客户端。
扩展知识:
套接字超时
在使用套接字的时候,读取数据之前当前线程会被阻塞,直到数据的到达为止,在这个过程中间可能由于被访问的主机不可达,从而使你的应用将等待很长的时间,最终受操作系统的底层影响而导致超时产生。也就是说,虽然最终会导致超时的产生,但我们在设计一个套接字的时候应该给定一个合理的超时时间,从而保证在读、写操作未完成之前可以有一个限定时间,当超出该限定时间时操作仍未完成,那么套接字就会抛出SocketTimeoutException异常,这样,我们就可以捕获该异常并编写超时后的程序逻辑。这样,通过设置套接字的超时时间(对套接字内所有的操作而言)就可以保证程序因为某些原因而导致程序长时间的无作用等待。
通过调用套接字的setSoTimeout(int milliseconds)方法来设置套接字的超时时间,比如:
Socket s = new Socket(...);
s.setSoTimeout(10000);
上面方法将设置套接字的超时时间为10秒,当10秒内数据仍未读取时,套接字将抛出异常并停止操作。注意,setSoTimeout方法会抛出SocketTimeoutException异常,并且实现了线程同步。
在上面我们所说的是当操作被阻塞的情况下,还有一种情况也会导致程序长期阻塞直至建立服务器的连接为止。回顾一下套接字的构造方法,其中一构造方法如下:Socket(String host, int port),如果该构造方法在连接制定的host由于某些特殊原因(网络)导致阻塞,那么该套接字将无限阻塞,直到建立和主机的连接为止。为了避免这种可能,我们还应该设置套接字的连接超时,当在指定时间内仍不能建立和主机的连接,则抛出异常并终止操作。解决步骤如下:
//先建立一个无连接套接字
Socket s = new Socket();
//在调用connect方法去指定套接字连接主机时给定连接超时参数,这里超时时间设置为10秒
s.connect(new InetSocketAddress(host,port),10000);
(InetSocketAddress类是一个专门封装套接字地址的特定类,可以通过该类将一个Scoket与指定主机及端口连接)
SocketChannel类
不知大家有没有发现,虽然给线程设置中断标志可以终止一个被阻塞线程,但是当线程是被套接字等网络因素而阻塞的情况下,设置中断标志是无法终止阻塞的。在1.4之后提供的java.nio.channels包中SocketChannel类则很好的解决了这个问题,即如果我们是通过该类来创建一个套接字连接,那么就可以使用线程的中断标志来终止被阻塞的线程,具体操作如下:
//首先,我们应该获取套接字连接对象,通过SocketChannel类的静态方法open可以得到一个指定主机连接对象
SocketChannel channel = SocketChannel.open(new InetSocketAddress("192.168.0.1",8001));
//对SocketChannel的对象进行的操作和对Socket的操作类似,可以通过Channels类的静态方法newOutputStream()于newInputStream()来分别得到通道中的输出和输入流
OutputStream out = Channels.newOutputStream(channel);
InputStream input = Channels.newInputStream(channel);
//或者通过Scanner来得到输入流
Scanner scan = new Scanner(channel);
注意,通过Scanner来得到输入流于通过Channels.newInputStream(channel)得到的输入流有所不同,实际上channel并没有与之关联的流,其所拥有的read和write方法都是通过Buffer对象来实现的。也就是说,Channels类的newInputStream方法得到的流实际上是通过Buffer来读取数据,而Scanner则是直接去读数据而不使用缓存。因此,读取大数据时使用缓冲可能更高效,否则反之。
SocketChannel实现了两个接口:ReadableByteChannel和WritableByteChannel,分别实现了read与write方法,这两个方法均接收ByteBuffer(java.nio)对象并对其操作,即使用SocketChannel的同时我们需结合nio包中的字节缓冲对象共同实现数据的读写操作。在使用SocketChannel时,假设线程正在执行打开、读取、写入操作,此时如果对线程进行中断,那么这些操作不会导致线程阻塞而是以抛出异常的方式结束。
总结
- IP(Internet Protocol)协议是一种低级路由协议,该协议主要实现将传输数据分解成许多小包,接着通过网络传到一个指定地址,但是,请注意,IP协议并不会保证传输的数据包一定到达目的地!TCP(Thransfer Control Protocol)协议正好弥补了IP协议的不足,属于一种较高级的协议,它实现了数据包的有力捆绑,通过排序和重传来确保数据传输的可靠!
- TCP与UDP协议均属于传输层协议,而IP协议属于网络层协议
- TCP/IP套接字用于在主机和Internet之间建立的可靠、双向、点对点、持续的流式连接
- UDP协议是一种基于数据报的快速的(因为它无需花时间去保证数据是否损坏,无需花时间确定接受方是否存在并等待响应)、无连接的、不可靠的信息包传输协议。
- ServerSocket被设计成在等待客户建立连接之前不做任何事情的监听器,Socket类为建立连向服务器套接字及启动协议交换而设计,当进程通过网络进行通信的时候,java技术使用流模型来实现数据的通信
- 数据报(Datagrams)是一种在不同机器之间传递的信息包,该信息包一旦从某一机器被发送给指定目标,那么该发送过程并不会保证数据一定到达目的地,甚至不保证目的地的存在真实性。
- DatagramPacket是一个数据容器,用来保存即将要传输的数据;DatagramSocket实现了发送和接收DatagramPacket的机制,即实现了数据报的通信方式。
- net.InetAddress类是java的IP地址封装类,内部隐藏了IP地址,可以通过它很容易的使用主机名以及IP地址
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)