1、网络模型:OSI参考模型和TCP/IP参考模型
通常用户操作的是应用层,而编程人员需要做的是传输层和网际层,用户在应用层操作的数据,经过逐层封包,最后到物理层发送到另一个模型中,再进行逐层解包,图示为:
![package1](https://img-blog.csdn.net/20150812000227184)
2、网络通信三要素:IP地址,端口号,传输协议
1.IP地址
1. 它是网络中的设备标识
2. 不易记忆,可用主机名表示,两者存在映射关系
3. 本机回环地址:127.0.0.1,主机名为:localhost。
IP地址:java中对应的是InetAddress类,存在于java.net包中。
InetAddress类:
(一)无构造函数,可通过getLocalHost()方法返回本地主机获取
InetAddress对象,此方法是静态的。
InetAddress i = InetAddress.getLocalHost();
(二)方法:
1. static InetAddress getByName(String host):获取指定主机的IP和主机名。(最好用ip地址去获取,主机名需要解析)
2. static InetAddress[] getAllByName(String host):在给定主机名的情况下,根据系统上配置的名称服务返回IP地址所组成的数组。返回对象不唯一时,用此方法。
3. String getHostAddress():返回IP地址字符串文本形式,以IP地址为主。
4. String getHostName():返回IP地址主机名。
(三)如何获取任意一台主机的IP地址对象:
1. 功能:返回InetAddress对象
2. 对于任意主机,需要指定传入主机名的参数
注意:如果IP地址和对应的主机名,这种映射关系没有在网络上,就不会解析成功,返回的还是指定的IP。
import java.net.InetAddress;
import java.net.UnknownHostException;
public class IpDemo {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
//获取我的本机信息
InetAddress ip = InetAddress.getByName("LQX");
//本机名和IP的打印
System.out.println("IP:"+ip.getHostAddress()+"\tname="+ip.getHostName());
//这个是我在hosts文件中定义的一个对应关系
InetAddress ip2 = InetAddress.getByName("www.lqx.com");
System.out.println("IP:"+ip2.getHostAddress()+"\tname:"+ip2.getHostName());
//查询百度网站的IP,百度应该不止有一个主机
InetAddress[] baidu=InetAddress.getAllByName("www.baidu.com");
for (InetAddress b :baidu)
{
String baddress=b.getHostAddress();
String bname=b.getHostName();
System.out.println("baiduIP="+baddress+"\tbaiduname="+bname);
}
}
}
打印结果:
2.端口号
1. 定义:
随着计算机网络技术的发展,原来物理上的接口(如键盘、鼠标、网卡、显示卡等输入/输出接口)已不能满足网络通信的要求,TCP/IP协议作为网络通信的标准协议就解决了这个通信难题。TCP/IP协议集成到操作系统的内核中,这就相当于在操作系统中引入了一种新的输入/输出接口技术,因为在TCP/IP协议中引入了一种称之为"Socket(套接字)"应用程序接口。有了这样一种接口技术,一台计算机就可以通过软件的方式与任何一台具有Socket接口的计算机进行通信。端口在计算机编程上也就是"Socket接口"。
a、用于标识进程的逻辑地址,不用进程的标识。
b、有效端口:0 ~65535,系统使用或保留的端口是:0~ 1024。
3.传输协议
即通信规则,包含TCP和UDP协议
一. UDP(User Datagram Protocol)用户数据报协议
1. 一种无连接的传输层协议,位于第四层——传输层,处于IP协议的上一层
2. 当报文发送之后,是无法得知其是否安全完整到达的
3. 资源消耗小,处理速度快的优点
所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。比如我们聊天用的ICQ和QQ就是使用的UDP协议。
二. TCP(Transmission Control Protocol 传输控制协议)
1. 一种面向连接的、可靠的、基于字节流的传输层通信协议,位于第四层——传输层,和UDP协议位于同一层
2. 通过三次握手完成连接,是可靠的协议
3. 必须建立连接,效率稍慢
TCP三次握手的过程如下:
(1)
客户端发送报文给服务器进入SEND状态。
(2)
服务器端收到报文,回应一个ACK报文,进入RECV状态。
(3)
客户端收到服务器端的SYN报文,回应一个ACK报文,进入Established
建立状态。
3、JAVA的UDP和TCP编程
1.Socket套接字
1. 用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信
2.它被称之为插座,相当于港口一样,是网络服务提供的一种机制。
3.通信两端都要有Socket,才能建立服务。
4.网络通信其实就是Socket间的通信,数据在两个Socket间通过IO传输。
2.UDP传输
在Java API中,实现UDP方式的编程,包含客户端网络编程和服务器端网络编程,主要由两个类实现,分别是:
1. DatagramSocket:UDP客户端编程涉及的步骤也是4个部分:建立连接、发送数据、接收数据和关闭连接。
DatagramSocket ds = new DatagramSocket();
这样就建立了一个链接,但是没有指定端口,系统会使用未被占用的端口,一般常用来做发送端。
DatagramSocket receive = new DatagramSocket(10001);
这种定义常用来做接收端,他会一直监听10001端口。
2. DatagramPacket:这个类中封装了许多传输的信息,对象中包含发送到的地址、发送到的端口号以及发送的内容等。
DatagramPacket dp = new DatagramPacket(byte[] buf, int length, InetAddress address, int port) ;
IO编程在UDP方式的网络编程中变得不是必须的内容。
接着,介绍一下UDP客户端编程中发送数据的实现。在UDP方式的网络编程中,IO技术不是必须的,在发送数据时,需要将需要发送的数据内容首先转换为byte数组,然后将数据内容、服务器IP和服务器端口号一起构造成一个DatagramPacket类型的对象,这样数据的准备就完成了,发送时调用网络连接对象中的send方法发送该对象即可。
/***
* UDP发送端:
* 需求:通过udp传输方式,将一段文字数据发送出去
*/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class Send {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.获取目标地址(本机地址)
InetAddress ip = InetAddress.getLocalHost();
//2.发送端的声明
DatagramSocket send = new DatagramSocket();
//3.要发送的字符串
String s = "Hello my name is Luan";
//转byte
byte []b = s.getBytes();
//4.传送字节数组b,目标地址为本机,端口10000
DatagramPacket packet = new DatagramPacket(b, b.length, ip, 10000);
//5.使用DatagramSocket类中的Send方法发送数据包
send.send(packet);
//6.资源关闭
send.close();
}
}
/***
* UDP接收端
* 需求:定义一个应用程序,用于接收udp协议传输的数据并处理。
*/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class Receive {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.接收端需要提供端口,实时监听
DatagramSocket recv = new DatagramSocket(10000);
//2.定义数据包,用于存储数据
byte b[] = new byte[1024];
//定义收到的包是放在字节数组b里的
DatagramPacket packet = new DatagramPacket(b, b.length);
//3.receive方法获取数据包
recv.receive(packet);//阻塞式方法:没有数据就不继续往下执行,至到得到数据。
String s = new String(b, 0, packet.getLength());
System.out.println(packet.getAddress()+"::"+packet.getPort()+"::"+s);
//4.关闭资源
recv.close();
}
}
信息传过来了,ip地址是没有问题的,我上的校园网分发的IP就是他。端口号是发送端的端口号,没有问题。
/***
* UDP的发送端
* 需求:用键盘录入的方式,来发送数据
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
public class InputSend {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.获取目标地址(本机地址)
InetAddress ip = InetAddress.getLocalHost();
//2.发送端的声明
DatagramSocket send = new DatagramSocket();
DatagramPacket packet = null;
//3.输入的字符读取到内存
BufferedReader in
= new BufferedReader(new InputStreamReader(System.in));
String s =null;
while((s=in.readLine())!=null)
{
//如果发的是“88”,则资源关闭
if(s.equals("88"))
{
break;
}
//转byte
byte []b = s.getBytes();
//传送字节数组b,目标地址为本机,端口10000
packet = new DatagramPacket(b, b.length, ip, 10000);
//使用DatagramSocket类中的Send方法发送数据包
send.send(packet);
}
send.close();
}
}
/***
* 接收端
*/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class InputReceive {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.接收端需要提供端口,实时监听
DatagramSocket recv = new DatagramSocket(10000);
//2.定义数据包,用于存储数据
while(true)
{
byte b[] = new byte[1024];
//定义收到的包是放在字节数组b里的
DatagramPacket packet = new DatagramPacket(b, b.length);
//3.receive方法获取数据包
recv.receive(packet);//阻塞式方法 :没有数据就不继续往下执行,至到得到数据。
String s = new String(b, 0, packet.getLength());
System.out.println(packet.getAddress()+"::"+packet.getPort()+"::"+s);
}
}
}
发送端:
接收端:
/***
*需求:UDP的客户端的聊天程序
*需要用到线程的使用:发送端和接收端分别做成两个线程
*共同组成了这个客户端的进程
*/
import java.io.*;
import java.net.*;
public class UDPTalk {
public static void main(String[] args) {
// TODO Auto-generated method stub
Send s1 = new Send();
receive r1 = new receive();
Thread t1 = new Thread(s1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
}
}
//接收端
class Send implements Runnable {
private DatagramSocket send = null;
private DatagramPacket packet = null;
@Override
public void run() {
// TODO Auto-generated method stub
try {
InetAddress ip = InetAddress.getLocalHost();
send = new DatagramSocket();
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
String s =null;
while((s=in.readLine())!=null)
{
//如果发的是“88”,则资源关闭
if(s.equals("88"))
{
break;
}
//转byte
byte []b = s.getBytes();
//传送字节数组b,目标地址为本机,端口10000
packet = new DatagramPacket(b, b.length, ip, 10000);
//使用DatagramSocket类中的Send方法发送数据包
send.send(packet);
}
send.close();
} catch (Exception e) {
// TODO Auto-generated catch block
throw new RuntimeException("发送数据失败");
//System.out.println("目的主机无效!");
}
}
}
//接收端
class receive implements Runnable
{
private DatagramSocket recv = null;
private DatagramPacket packet = null;
@Override
public void run() {
// TODO Auto-generated method stub
//1.接收端需要提供端口,实时监听
try {
recv = new DatagramSocket(10000);
while(true)
{
byte b[] = new byte[1024];
//定义收到的包是放在字节数组b里的
DatagramPacket packet = new DatagramPacket(b, b.length);
//3.receive方法获取数据包
recv.receive(packet);//阻塞式方法 :没有数据就不继续往下执行,至到得到数据。
String s = new String(b, 0, packet.getLength());
System.out.println(packet.getAddress()+"::"+packet.getPort()+"::"+s);
}
} catch (Exception e) {
// TODO Auto-generated catch block
throw new RuntimeException("接收端接收数据失败");
}
}
}
只能自己和自己聊了,如果想和别人聊,那么要把目标IP改为广播地址。
比如我这个就应该是111.114.116.255,那么大家都往这个IP发送内容的话就可以一起聊天了。
2.TCP传输
TCP传输,需要明确的区分客户端和服务端。
客户端使用的类是:Socket,他的构造方法会指定目标地址和端口;
Socket client1 = new Socket(InetAddress address, int port);两个基本方法:
public InputStream getInputStream():返回此套接字的输入流,Socket对象调用public OutputStream getOutputStream():返回套接字的输出流,Socket对象调用
服务器端使用的类:ServerSocket,他的构造方法只需制定需要监听的端口号即可。
ServerSocket server = new ServerSocket(int port);
基本方法:
public Socket accept():返回值是一个Socket对象,也就是说在服务器端开辟了一个空间,这个空间专门为对应的客户端服务。可以理解为,只要客户端对服务器有请求,那么在服务器就会获取这个客户端的Socket对象,并使用这个服务器端的客户端Socket对象与客户端交流。
/***
* 客户端代码
* 需求:客户端给服务端发送数据,服务端收到后,给客户端反馈信息。
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//目标IP获取
InetAddress ip = InetAddress.getLocalHost();
//1.建立客户端Socket,指定目的主机和端口
Socket client1 = new Socket(ip, 10000);
//2.获取Socket流中输出流,发送数据
OutputStream ou = client1.getOutputStream();
String s = "这是我的第一个TCP编程!";
byte b[] = s.getBytes();
ou.write(b);
//对服务器返回的数据进行接收
InputStream in =client1.getInputStream();
byte b2[] = new byte[1024];
int len2 = in.read(b2);
String s2 = new String(b2, 0, len2);
System.out.println(s2);
client1.close();
}
}
/***
* 服务器端
*需求: 响应客户端的请求
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//1.服务端使用的是ServerSocket类
ServerSocket server = new ServerSocket(10000);
//2.accept方法会获得一个Socket对象,并侦听此对象
Socket client = server.accept();
//3、获取对应客户端对象的读取流读取发过来的数据,并打印
InputStream in = client.getInputStream();
byte b[] = new byte[1024];
int len = in.read(b);
String s = new String(b, 0, len);
System.out.println(s);
//对客户端的回复
OutputStream ou = client.getOutputStream();
ou.write("我是服务器,收到了".getBytes());
server.close();
}
}
服务器端显示收到了:
客户端收到了服务器端的响应:
/***
* 练习
* 需求:建立一个文本转换服务器
* 客户端给服务端发送文本,服务端会将文本转成大写再返回给客户端。
* 而且客户端可以不断的进行文本转换。当客户端输入over时,转换结束。
*/
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
public class UpCaseClient1 {
public static void main(String[] args) throws IOException, IOException {
// TODO Auto-generated method stub
//创建socket服务
Socket client = new Socket("127.0.0.1", 10000);
//定义目的,将数据写入到Socket输出流。发给服务端
PrintWriter pw = new PrintWriter(client.getOutputStream(), true);
//定义读取键盘数据的流对象
BufferedReader br1 =
new BufferedReader(new InputStreamReader(System.in));
//定义一个Socket读取流,读取服务端返回的大写信息。
BufferedReader br2 =
new BufferedReader(new InputStreamReader(client.getInputStream()));
String s = null;
String ret = null;
while((s=br1.readLine())!=null)
{
//判断,是“over”,结束
if(s.equals("over"))
{
break;
}else{
pw.println(s);//发送
ret = br2.readLine();//读取返回的信息
System.out.println(ret);
}
}
client.close();
}
}
/**
* 服务器端
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class UpCaseServer1 {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
//创建服务端的ServerSocket服务,并指定监听端口
ServerSocket server = new ServerSocket(10000);
//获取客户端连接
Socket client1 = server.accept();
//读取Socket流中的数据
BufferedReader br1 =
new BufferedReader(new InputStreamReader(client1.getInputStream()));
//将大写数据写入到Socket输出流,并发送给客户端。
PrintWriter pw = new PrintWriter(client1.getOutputStream(), true);
String s = null;
while((s = br1.readLine())!=null)
{
if(s.equals("over"))
{
break;
}else{
//将读到数据转换为大写后返回
s = s.toUpperCase();
pw.println(s);
System.out.println(s);
}
}
server.close();
}
}
这里遇到的问题主要是:由于Writer类实现了Flushable接口,调用 flush 方法将所有已缓冲输出写入底层流。
所以,出现的两边都在等待的问题,第一点就是因为,数据没有实时刷新造成的,那么需要调用flush方法手动刷进去。
错误案例,客户端和服务器端都没有反应
/**客户端
*/
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
public class UpCaseClient1 {
public static void main(String[] args) throws IOException, IOException {
//创建socket服务
Socket client = new Socket("127.0.0.1", 10000);
//定义目的,将数据写入到Socket输出流。发给服务端
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
//定义读取键盘数据的流对象
BufferedReader br1 =
new BufferedReader(new InputStreamReader(System.in));
//定义一个Socket读取流,读取服务端返回的大写信息。
BufferedReader br2 =
new BufferedReader(new InputStreamReader(client.getInputStream()));
String s = null;
String ret = null;
while((s=br1.readLine())!=null)
{
//判断,是“over”,结束
if(s.equals("over"))
{
break;
}else{
bw.write(s+"\r\n");
//bw.newLine();//换行
//bw.flush();//此处是重点,如果此处不打开,那么数据将会不再刷新出去,那么客户端和服务器端都在等待。
ret = br2.readLine();//读取返回的信息
System.out.println(ret);
}
}
client.close();
}
}
/**
* 服务器端
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class UpCaseServer1 {
public static void main(String[] args) throws IOException {
//创建服务端的ServerSocket服务,并指定监听端口
ServerSocket server = new ServerSocket(10000);
//获取客户端连接
Socket client1 = server.accept();
//读取Socket流中的数据
BufferedReader br1 =
new BufferedReader(new InputStreamReader(client1.getInputStream()));
//将大写数据写入到Socket输出流,并发送给客户端。
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client1.getOutputStream()));
String s = null;
while((s = br1.readLine())!=null)
{
if(s.equals("over"))
{
break;
}else{
//将读到数据转换为大写后返回
s = s.toUpperCase();
//pw.println(s);
bw.write(s+"\r\n");
//bw.newLine();//换行
//bw.flush();//此处重点同上
System.out.println(s);
}
}
server.close();
}
}
问题:
上面的代码运行不了,归根结底就是因为,Write继承了接口 Flushable,那么,每次服务器读取一行,如果读不到换行标志,则等待;客户端读取键盘输入,读取后不使用flush方法刷出去,那么积压住,两边都等待,造成两边都是等待的问题
解决办法:
1.手动添加换行标志。
2.使用PrintWriter pw = new PrintWriter(client.getOutputStream(), true);treu的意思为自动刷新,且提供prinlln()方法,免去了手动添加换行和刷新的麻烦。
/***
* 需求:向服务器上传一个文件,服务返回一条信息
* 1、客户端:
* 源:硬盘上的文件;目的:网络设备,即网络输出流。
* 若操作的是文本数据,可选字符流,并加入高效缓冲区。若是媒体文件,用字节流。
* 2、服务端:
* 源:socket读取流;目的:socket输出流。
*
* 3、出现的问题:
* 现象:
* a、文件已经上传成功了,但是没有得到服务端的反馈信息。
* b、即使得到反馈信息,但得到的是null,而不是“上传成功”的信息
*
* 原因:
* a、因为客户端将数据发送完毕后,由于双发并没有指定结束标志,那么服务端仍然在等待读取数据,双方都在等待。
* b、上个问题解决后,收到的不是指定信息而是null,是因为服务端写入数据后,需要刷新,才能将信息反馈给客服端。
*
* 解决a:
* 方法一:定义结束标记,先将结束标记发送给服务端,让服务端接收到结束标记,然后再发送上传的数据。但是这样定义可能会发生定义的标记和文件中的数据重复,而导致提前结束。
* 方法二:定义时间戳,由于时间是唯一的,在发送数据前,先获取时间,发送完后在结尾处写上相同的时间戳,在服务端,接收数据前先接收一个时间戳,然后在循环中判断时间戳以结束标记。
* 方法三:通过socket方法中的shutdownOutput(),关闭输入流资源,从而结束传输流,以给定结束标记。通常用这个方法。
* 解决b:
* 方法一:若使用 BufferedWriter bwout=new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); 则需要调用flush()方法刷新
* 方法二:直接使用PrintWriter pw2 = new PrintWriter(client.getOutputStream(),true);自动刷新。
*/
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
/***
* 练习
* 需求:向服务器上传一个文件,服务器返回一条信息
* @author LQX
*
*/
public class UpLoadClient {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
Socket client = new Socket("127.0.0.1", 10000);
//文件的读取
BufferedReader br1 =
new BufferedReader(new FileReader("f:\\DemoClass.java"));
//定义目的,将数据写入到Socket数据流中,并发送给服务端
//此处我设置自动刷新,方便操作,也可以使用BufferedReader,那么后面就必须手动刷新
PrintWriter pw = new PrintWriter(client.getOutputStream(), true);
//读取Socket读取流中的数据
BufferedReader br2 =
new BufferedReader(new InputStreamReader(client.getInputStream()));
String s = null;
while((s = br1.readLine())!=null)
{
pw.println(s);
}
/**
* 通过socket方法中的shutdownOutput(),关闭输入流资源,
* 从而结束传输流,以给定结束标记。通常用这个方法。
* 不加这个,那么,服务器端没有结束标志,仍然等待客户端发送数据。
*/
client.shutdownOutput();//关闭客户端的输出流。相当于给流中加入一个结束标记-1.
System.out.println(br2.readLine());
client.close();
}
}
/***
* 服务器端
* 主要使用了PrintWriter(OutputStream out, boolean autoFlush) 构造方法
* 直接指定了自动刷新,比较方便
*
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class UpLoadServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(10000);
//获取客户端链接
Socket client = server.accept();
//读取Socket读取流中的数据
BufferedReader br =
new BufferedReader(new InputStreamReader(client.getInputStream()));
//
PrintWriter pw1 = new PrintWriter(new FileWriter("f:\\Copy.txt"),true);
//将返回信息写入Socket流的写入流中
PrintWriter pw2 = new PrintWriter(client.getOutputStream(),true);
String s = null;
while((s=br.readLine())!=null)
{
System.out.println(s);
pw1.println(s);
}
pw2.println("接收成功!");
server.close();
}
}