概述
java除了可以完成本地的操作,也可以完成网络通讯。比如想从自己的电脑上发送一个信息到张三的电脑上,张三收到信息之后再给我返回一个信息,利用java实现两个机器之间的数据的通讯。数据通讯的原理就是数据传输的过程,与本机的区别就是涉及到网络。
网络通讯要具备的要素和模型:
比如和张三通讯
1、首先要找到张三的主机,张三主机的标识就是IP地址(也就是主机的名字,IP地址由4个字节表示,可以表示很多主机,避免冲突)。
2、和张三通讯的方式有很多种,可以是QQ,也可以是微信。两个机器都要装有通讯的软件QQ或者微信。数据要发送到对方指定的应用程序上,为了标识这些应用程序,所以给这些网络应用程序都用数字进行标识,比如QQ用4000标识,微信用2900标识,为了方便称呼这个数字,叫做端口 ,这是逻辑端口,不是物理接口,也不是网线接口。
3、两个机器想要进行通讯,还要定义通信规则,使两方都能听懂对方的内容,这个通信规则就是协议。互联网的成千上万的机器都能进行通讯,因为国际组织定义了一个通用的协议TCP/IP,现在的操作系统里面都安装了这个协议 ,适用于广域网和局域网。
可以添加新的协议。也可以卸载TCP/IP协议,但是一般的机器卸载不了,因为已经固化到系统里面了。
但是某些特有的单位和组织,为了安全,他们的通讯方式和我们不一样,他们有自己特有的协议进行通讯,所以外界不能与之通讯,入侵不了。
IP地址介绍:
- IP地址是由4段组成,每段是个字节,最大值是255
- IP地址分成很多段,A/B/C等
- 有个IP很特殊127.0.0.1,这是本地回环地址,当本机没有配地址时,本机默认的地址就是127.0.0.1,其中一个用处是用来测试网卡,ping 127.0.0.1 如果成功说明网卡正常。
- 有些IP地址被保留不用于公网,用于局域网中。不同局域网中可以有相同的IP地址。192.168.. 是最常用的保留地址段,还有其他保留地址段。
- 子网掩码的出现是为了解决电脑数量增多,IP地址不够用的问题。电信厂商们给某个区域只提供一个公网IP,通过子网掩码将这个区域内划分为局域网,整个区域走同一个公网IP。
- 后来四段IP地址不够用了,便出现了六段IP地址,而且出现字母,数量多得多。
端口介绍
端口的大小:0-65535
其中0-1024端口被系统使用了,自己的程序也可以用,但是可能出现冲突现象。
几个默认端口:web服务:80、tomcat服务器:8080、MySql数据库:3306。可以自己定义端口。
网络模型
ISO网络模型中传输过程简单描述:
以QQ传送消息为例解释。
QQ软件在应用层上,首先将数据按照应用层的封装规则对数据进行封装。
然后传到表示层,再按照表示层的规则进行封装,然后进行会话层封装,传输层封装,网络层封装(加上IP地址),数据链路层封装,最后到达物理层,物理层就是网线、光纤、无线 等。这个过程叫做数据封包过程,根据每一层的协议加上每层的信息。数据封包之后,最后经过物理层传输到目的地址。
在目的地址会按照每层协议进行数据拆包,最终到达应用层,在应用层根据端口号确定将数据传给QQ软件。
TCP/IP模型
由于ISO的七层模型理解起来比较麻烦,后来出现了TCP/IP模型,将7层模型简化成了4层模型。将应用层、表示层、会话层归为应用层;将数据链路层和物理层归为主机至网络层,在加上传输层和网络层一共是4层。
我们进行Java网络编程就是在传输层和网际层,而Java Web开发,是在应用层,将底层的东西进行了封装。
传输层协议常见的有TCP、UDP
网际层最常见的协议IP
应用层的协议有很多比如HTTP和FTP等。
网络通讯要素
IP地址、端口号、传输协议。
Java语言进行网络通讯时,这三个要素是怎么体现的呢?
Java提供三个对象来操作三个要素。
IP地址:类 InetAddress 此类表示互联网协议 (IP) 地址。
IP 地址是 IP 使用的 32 位或 128 位无符号数字,它是一种低级协议,UDP 和 TCP 协议都是在它的基础上构建的。
没有构造函数。
获得对象的方式:static InetAddress getLocalHost() 返回本地主机
主要方法: String getHostAddress()
返回 IP 地址字符串(以文本表现形式)。
String getHostName()
获取此 IP 地址的主机名。
static InetAddress getByName(String host)
在给定主机名的情况下确定主机的 IP 地址。
端口号:用于标识进程的逻辑地址,不同进程的标识。有效端口号0~65535.其中0~1024系统使用或者保留端口。
传输协议:
两种传输协议比较常见:TCP和UDP,区别是什么?
UDP(面向无连接,发送数据之前双方不需要建立连接,类似邮局寄包裹):
- 将数据及源和目的封装成数据包中,不需要建立连接
- 每个数据包的大小限制在64K
- 因无连接,是不可靠协议
- 不需要建立连接,速度快
聊天通讯使用的就是UDP。网络视频也是UDP。对传输速度要求高,可靠性要求低的网络传输一般是UDP。
TCP(面向连接,双方必须都在(三次握手确定),相当于打电话):
- 建立连接,形成传输数据
- 在连接中进行大数据量传输 (不需要封装包)
- 通过三次握手完成连接,是可靠协议
- 必须建立连接,效率稍低 ,消耗资源
文件下载使用的就是TCP,可靠性要求高
Socket
Java网络编程指的就是Socket编程。Socket是插座(也称套接字)的意思。
Socket为网络服务提供一种机制。
两个主机如果想要通讯,需要有物理层的连接,比如网线,主机上都有一个网线的插口,连接两个主机。每个应用程序都有一个类似的插口,使两个主机上的同种应用程序之间可以网络通讯。这个插口就是Socket的概念。
所以,通信的两端都有Socket,网络通信其实就是Socket间的通信,数据在两个Socket之间通过IO传输。
每种传输协议对应的建立Socket端点的方式,就有了UDP传输方式中Socket服务建立方式和TCP传输方式中Socket服务建立方式。
UDP传输方式
DatagramSocket类 此类表示用来发送和接收数据报包的套接字(插座)。
构造方法:
DatagramSocket()
构造数据报套接字并将其绑定到本地主机上任何可用的端口(随机)。
DatagramSocket(int port)
创建数据报套接字并将其绑定到本地主机上的指定端口(接收端和发送端都可以指定绑定的端口号)。
主要方法:
void receive(DatagramPacket p)
从此套接字接收数据报包。
void send(DatagramPacket p)
从此套接字发送数据报包。
void close()
关闭此数据报套接字。
DatagramPacket类 此类表示数据报包。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
构造方法:
DatagramPacket(byte[] buf, int length)
构造 DatagramPacket,用来接收长度为 length 的数据包。
部分方法:
InetAddress getAddress()
返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。
byte[] getData()
返回数据缓冲区。
小实例:
需求:创建两个应用程序,一个用于发送数据,一个用于接收数据,接收数据的端口设为10000。
思路:
发送端
1,建立udpsocket服务
2. 提供数据,并将数据封装到数据包中
3. 通过socket服务的发送功能,将数据包发送出去
4.关闭资源
接收端:
1,建立udpsocket服务,通常会监听一个端口,其实就是给这个接收网络应用程序定义数字标识, 方便于明确哪些数据过来,该应用程序可以处理。
2. 定义一个数据包,因为要存储接收到的字节数据,且数据包对象中有更多功能可以
提取字节数据中的不同数据信息
3. 通过socket服务的接收功能,将数据包存入已经定义好的数据包中。
4. 通过数据包对象的特有功能,将这些不同的数据取出,打印在控制台上
5. 关闭资源
import java.net.*;
class UdpSendDemo
{
public static void main(String[] args) throws Exception
{
DatagramSocket ds=new DatagramSocket();
byte[] data="udp is coming".getBytes();
DatagramPacket dp=new DatagramPacket(data,data.length,InetAddress.getByName("127.0.0.1"),10000);
ds.send (dp);
ds.close();
}
}
import java.net.*;
class UdpReceiveDemo
{
public static void main(String[] args) throws Exception
{
DatagramSocket ds=new DatagramSocket(10000);
byte[] data=new byte[1024];
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.receive(dp);//阻塞式方法,没有接收到数据就等
InetAddress ip=dp.getAddress();
System.out.println("主机名"+ip.getHostName());
System.out.println("IP"+ip.getHostAddress());
System.out.println("数据:"+new String(dp.getData(),0,dp.getLength()));
}
}
使用UDP方式编写简单的聊天程序
要求:
编写一个聊天程序。
有收数据的部分,和发数据的部分。
这两部分需要同时执行。
那就需要用到多线程技术。
一个线程控制收,一个线程控制发。
因为收和发动作是不一致的,所以要定义两个run方法。
而且这两个方法要封装到不同的类中。
发送类:
class SendThread implements Runnable
{
private DatagramSocket ds=null;
public SendThread(DatagramSocket ds)
{
this.ds=ds;
}
public void run()
{
try
{
BufferedReader bur=new BufferedReader(new InputStreamReader(System.in));
String line=null;
while((line=bur.readLine())!=null)
{
byte[] senddata=line.getBytes();
DatagramPacket dp=new DatagramPacket(senddata,senddata.length,InetAddress.getByName("192.168.1.114"),10001);
ds.send(dp);
//System.out.println("IP:"+InetAddress.getLocalHost().getHostName()+":"+line);
}
}
catch (Exception e)
{
throw new RuntimeException("发送失败");
}
}
}
接收类:
class ReceiveThread implements Runnable
{
private DatagramSocket ds=null;
public ReceiveThread(DatagramSocket ds)
{
this.ds=ds;
}
public void run()
{
try
{
while(true)
{
byte[] data=new byte[1024];
DatagramPacket dp=new DatagramPacket(data,data.length);
ds.receive(dp);//阻塞式方法,没有接收到数据就等
InetAddress ip=dp.getAddress();
String receiveData=new String(dp.getData(),0,dp.getLength());
System.out.println("IP:"+ip.getHostAddress()+":"+receiveData);
}
}
catch (Exception e)
{
throw new RuntimeException("接收失败");
}
}
}
聊天类:
{
public static void main(String[] args) throws Exception
{
DatagramSocket sds=new DatagramSocket();
DatagramSocket rds=new DatagramSocket(10002);
new Thread(new SendThread(sds)).start();
new Thread(new ReceiveThread(rds)).start();
}
}
因为聊天是两个主机之间的通信,为了简单的测试程序,在同一主机上开启两个dos窗口测试。并且上面的代码只是通信的一方的代码,另外一方的代码没有差别,只是DatagramSocket和DatagramPacket绑定的端口不同:比如A的接收Socket服务对象的端口绑定为10002端口,发送的DatagramPacket绑定为对方的10001端口,而B的接收Socket服务对象的端口绑定为10001端口,发送的DatagramPacket绑定为对方的10002端口.两者的发送Socket服务不用绑定端口,让系统随机分配就好。听的好晕,看看代码就好了。简言之,发送数据时,用DatagramPacket确定接收者的端口,接收数据时,用DatagramSocket确定自身绑定的端口,这两个端口保持一致,就可以接收到数据啦。
TCP传输方式
UDP分的是发送端和接收端,而TCP分的是客户端和服务器端,分别对应两个对象:Socket和ServerSocket
客户端:
Socket类:此类实现客户端套接字(也可以就叫“套接字”)。
构造函数:
Socket(InetAddress address, int port)
创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
Socket(String host, int port)
创建一个流套接字并将其连接到指定主机上的指定端口号。
通过查阅Socket对象,发现在该对象建立时,就可以连接指定的主机。因为TCP是面向连接的,所以在建立Socket服务时,就要有服务端存在,并且连接成功。形成通路后,在该通道进行数据的传输。也有无参数的构造函数,此时用connect()方法确定目的端并连接。
Socket对象一旦建立成功,说明通信的通道已建立成功,便产生了Socket流也就是网络流。
Socket中封装了网络流,既有输入流也有输出流。可以用方法获得这两个流。
InputStream getInputStream()
返回此套接字的输入流。
OutputStream getOutputStream()
返回此套接字的输出流。 通过网络发送到对方主机上。
服务器端:
SocketServer类:此类实现服务器套接字。服务器套接字等待请求通过网络传入。
构造方法:
ServerSocket()
创建非绑定服务器套接字。
ServerSocket(int port)
创建绑定到特定端口的服务器套接字。
方法:
Socket accept()
侦听并接受到此套接字的连接。 这也是一个阻塞式方法。返回一个Socket对象。
多个客户端往服务端发送数据,服务端返回数据时,是如何做到那个客户端发来的,返回给哪个客户端,而不发生错误呢?
答:实际上,ServerSocket用accept()方法获得请求的客户端的Socket对象,利用该对象的输入流和输出流与该客户端通信,这样便不会发生发错对象的情况。而且,服务端的输入流和客户端的输出流对应,服务端的输出流和客户端的输入流对应。
TCP演示示例1:
客户端:
需求:给服务端发送一个文本数据
import java.io.*;
import java.net.*;
class TcpClient
{
public static void main(String[] args) throws Exception
{
//创建客户端的socket服务,指定目的主机和端口,
//一旦成功,通路建立,便有了Socket流即网络流,Socket流里面既有输入流也有输出流。
Socket s=new Socket("192.168.1.114",10003);
//为了发送数据,应该获取Socket流中的输出流
OutputStream os=s.getOutputStream();
os.write("tcp is coming".getBytes());
s.close();
}
}
服务端:
需求:定义端点接收数据并打印在控制台上。
服务端:
1,建立服务端的socket服务。ServerSocket();
并监听一个端口。
2,获取连接过来的客户端对象。
通过ServerSokcet的 accept方法。没有连接就会等,所以这个方法阻塞式的。
3,客户端如果发过来数据,那么服务端要使用对应的客户端对象,并获取到该客户端对象的读取流来读取发过来的数据。
并打印在控制台。
4,关闭客户端(服务端有可能不关闭自己,但会关闭客户端,节省资源)
5、关闭服务端。(可选)
class TcpServer
{
public static void main(String[] args) throws Exception
{
//建立服务端socket服务。并监听一个端口。
ServerSocket ss = new ServerSocket(10003);
//通过accept方法获取连接过来的客户端对象。
while(true)
{
Socket s = ss.accept();
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+".....connected");
//获取客户端发送过来的数据,那么要使用客户端对象的读取流来读取数据。
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf,0,len));
s.close();//关闭客户端.
}
//ss.close();
}
}
TCP演示示例2:
需求:客户端给服务端发送数据,服务端收到后,给客户端反馈信息。
思路:
程序基于示例1的程序进行简单修改即可。
客户端Socket对象的输出流写完数据之后,获得Socket的输入流,执行read()方法,这也是个阻塞式方法,若没读到数据,等待。
服务端读完数据之后,获得socket的输出流,写入反馈的内容即可。
class TcpClient2
{
public static void main(String[] args)throws Exception
{
Socket s = new Socket("192.168.1.254",10004);
OutputStream out = s.getOutputStream();
out.write("服务端,你好".getBytes());
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf,0,len));
s.close();
}
}
class TcpServer2
{
public static void main(String[] args) throws Exception
{
ServerSocket ss = new ServerSocket(10004);
Socket s = ss.accept();
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"....connected");
InputStream in = s.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf,0,len));
OutputStream out = s.getOutputStream();
Thread.sleep(10000);
out.write("哥们收到,你也好".getBytes());
s.close();
ss.close();
}
}
TCP练习
要求:
建立一个文本转换服务器。
客户端给服务端发送文本,服务断会将文本转成大写再返回给客户端。
而且客户端可以不断的进行文本转换。当客户端输入over时,转换结束。
分析:
客户端:
既然是操作设备上的数据,那么就可以使用io技术,并按照io的操作规律来思考。
源:键盘录入。
目的:网络设备,网络输出流。
而且操作的是文本数据。可以选择字符流。
步骤
1,建立服务。
2,获取键盘录入。
3,将数据发给服务端。
4,获取服务端返回的大写数据。
5,结束,关资源。
都是文本数据,可以使用字符流进行操作,同时提高效率,加入缓冲。
服务端:
源:socket读取流。
目的:socket输出流。
都是文本,装饰。
import java.io.*;
import java.net.*;
class TranseClient
{
public static void main(String[] args) throws Exception
{
Socket s=new Socket("192.168.1.6",10003);
BufferedReader bur=new BufferedReader(new InputStreamReader(System.in));
BufferedWriter buw=new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
BufferedReader burIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
String line=null;
while((line=bur.readLine())!=null)
{
System.out.println("想要转换的文本为"+line);
if(line.equals("over"))
break;
buw.write(line);
buw.newLine();
buw.flush();
String str=burIn.readLine();
System.out.println("转换后的文本为"+str);
}
bur.close();
s.close();
}
}
class TranseServer
{
public static void main(String[] args) throws Exception
{
ServerSocket ss=new ServerSocket(10003);
Socket s=ss.accept();
BufferedReader bur=new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter buw=new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line=null;
while((line=bur.readLine())!=null)
{
System.out.println("要转换的文本为:"+line);
if(line.equals("over"))
break;
buw.write(line.toUpperCase());
buw.newLine();
buw.flush();
}
s.close();
ss.close();
}
}
这个练习容易出现的问题。
现象:客户端和服务端都在莫名的等待。
为什么呢?
因为客户端和服务端都有阻塞式方法。这些方法么没有读到结束标记。那么就一直等
而导致两端,都在等待。
readLine()方法读取一个文本行。通过下列字符之一即可认为某行已终止:换行 (‘\n’)、回车 (‘\r’) 或回车后直接跟着换行,如果没有遇到换行符,那么会一直等待。所以当源字节流是键盘录入时,要在程序中手动写入换行符,或者调用BufferedWriter的newLine()方法。
还有一个地方比较特殊,当客户端close(),相当于在输出流的末尾写入-1,服务端读取到-1,根据程序关闭或者不关闭。