1. 简单了解网络通信协议TCP/IP网络模型相关名词
应用层(HTTP,FTP,DNS等) |
传输层(TCP,UDP) |
网络层(IP,ICMP等) |
链路层(驱动程序,接口等) |
- 链路层:用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对网线、光纤提供的驱动。
- 网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或网络。
- 传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
- 应用层:主要负责应用程序的协议,如HTTP协议,或者FTP协议等。
- IP地址:在网络中,需要指定一台电脑的唯一标示号,用于网络中任意两台电脑之间的通信,通过这个IP号来确定另一台电脑,在TCP/IP协议中,这个标识号就是ip地址,常用的ip版本是IPV4,也就是4个字节长度的二进制数表示的,通常会写成十进制的形式,每个字节的二进制数转换为10进制数(范围是0-255),比如127.0.0.1。
- 端口号:通过ip地址可以确定网络中要进行通信的计算机,而通过端口,就可以确定在这台计算机上要与那个应用程序进行通信。端口号是用两个字节的二进制数表示,通常会写成十进制(范围是0-65535),自己编写的应用程序所占用的端口要使用1024之后的端口,因为小于1024的端口一般都被占用了。
2. Java中网络编程的编写
2.1 IP地址类——java.net.InetAddress
1. 该类有两个直接子类 java.net.Inet4Address 和 java.net.Inet6Address 分别对应IPV4和IPV6,该类的对象无法通过构造器生成,只能通过静态方法来获取。
2. 相关常用API如下:
- public static InetAddress getByName(String host):通过一串IP地址字符串生成一个InetAddress对象
- public static InetAddress getLocalHost():获取本机的InetAddress对象,里面包含了本机的ip地址和主机名
- public static InetAddress getByAddress(byte[] addr):通过一串二进制码表示的ip地址来得到InetAddress
- public String getHostAddress():获取当前InetAddress对象中的主机名+ip地址
- public static InetAddress getByAddress(String host, byte[] addr):通过主机名+ip地址来获取对应的InetAddress对象
- public static InetAddress getByName(String host):通过ip地址字符串来获取对应的InetAddress对象
- public byte[] getAddress():获取当前InetAddress对象中IP地址的二进制字节码
import java.net.InetAddress;
import java.net.UnknownHostException;
public class IPAddress {
public static void main(String[] args) {
InetAddress inet;
try {
inet=InetAddress.getLocalHost();//获取本机的InetAddress对象
System.out.println(inet.toString());//输出的就是主机名和ip地址
String ip=inet.getHostAddress();//获取本机ip地址
String name=inet.getHostName();//获取主机名
System.out.println(ip+":"+name);
//通过ip地址来获取对应的InetAddress对象
byte[] addr={127,0,0,1};
inet2=InetAddress.getByAddress(addr);
System.out.println(inet2.toString());
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
2.2 TCP与UDP协议
1. UDP协议:是无连接通信协议,即在数据传输时,发送端与接收端不建立逻辑连接。也就是说,发送方在发送数据时,不会确定是否有接收方存在或者是否接收到数据,而接收方也不会向发送方反馈是否接收到数据;依据这种特点,UDP协议一般在网络直播、社交软件(如QQ)等应用较多,其不保证数据完整性。另外,UDP协议限制传输数据大小为64kb以内,否则发生数据丢失时丢失的数据过多而造成不安全的问题。
2. TCP协议:是面向连接的通信协议,即在传输数据前,发送端先要与接收到建立逻辑连接,然后再传输数据。TCP协议提供了发送端与接收端之间可靠无差错的数据传输。在TCP协议中,必须明确客户端与服务端,由客户端发出连接请求,每次连接的建立都需要进行“三次握手”。第一次握手,客户端向服务端发送连接请求;第二次握手,服务端向客户端发送一个响应,通知客户端已接收连接请求;第三次握手,客户端再次向服务端发送一个请求,表示确认连接信息,然后连接建立。由于其面向连接,所以可以保证数据传输的安全性,而且其支持大数据传输。
2.3 UDP通信的Java代码实现
1. jdk中提供了两个类实现UDP协议网络程序:
- java.net.DatagramPacket:数据打包类,用于将数据以UDP协议的格式打包,类似于“集装箱”的作用
- java.net.DatagramSocket:数据发送和接受,以UDP协议的格式打包的数据将通过此类对象发送至接收端,或在接收端使用该对象接收数据,类似于“码头”的作用。
2. 基本应用代码如下:实现一个UDP协议的发送端与接收端
/**
* @ClassName:UDPServer
* @Description:数据发送端
* 先将数据打包在一个java.net.DatagramPacket对象中
* 然后通过java.net.DatagramSocket对象发送至接收端
*/
public class UDPServer {
public static void main(String[] args) {
try {
send();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* @throws UnknownHostException
* @throws UnsupportedEncodingException
* @throws SocketException
* @Title:send
* @Description:发送数据
*/
public static void send() throws IOException{
DatagramPacket packet;
DatagramSocket socket;
//1. 创建DatagramPacket对象,封装数据,并且指定接收端的IP地址和端口号
byte[] data="hello".getBytes();
byte[] address={127,0,0,1};
InetAddress inet=InetAddress.getByAddress(address);
packet=new DatagramPacket(data, data.length , inet, 8888);
//2. 创建DatagramSocket对象,发送DatagramPacket对象数据包
socket=new DatagramSocket(8880);//在本机申请一个端口号
socket.send(packet);
//3. 关闭资源
socket.close();
}
}
/**
* @ClassName:UDPClient
* @Description:数据接收端
* 通过java.net.DatagramSocket对象接收到一个java.net.DatagramPacket对象
* 解析java.net.DatagramPacket对象,获取其中的数据
*/
public class UDPClient {
public static void main(String[] args) {
try {
get();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* @throws IOException
* @Title:get
* @Description:接收数据
*/
public static void get() throws IOException{
DatagramPacket packet;
DatagramSocket socket;
//1. 首先创建DatagramSocket对象,用于接收发送端传输的数据
socket=new DatagramSocket(8888);//必须指定一个端口号,而且要与发送端发送的DatagramPacket对象的端口号一致
//2. 创建字节数组,用于存储接收获取得数据
byte[] data=new byte[1024];
//3. 创建DatagramPacket对象,通过DatagramSocket的receive()方法接收数据
packet=new DatagramPacket(data, data.length);
//4. 接收数据,并且拆包分析
socket.receive(packet);//该方法是一个线程阻塞方法,如果没有接收到数据,就会一直阻塞
//拆包分析数据
InetAddress inet=packet.getAddress();//获取数据发送端ip地址对象
int length=packet.getLength();//获取数据字节数组真实长度
System.out.println(new String(data,0,length));
//5. 关闭资源
socket.close();
}
}
2.4 TCP通信的Java代码实现
1. jdk中提供了两个类实现TCP协议网络程序:
- java.net.ServerSocket,服务端接收数据与发送数据,创建该对象相当于建立一个服务器端,等待着客户端的连接请求
- java.net.Socket,客户端接受与发送数据,创建该对象时,可以向指定ip与端口的服务器端发送连接请求,建立连接后即可开始通信
2. 一个简单的TCP网络程序演示基本用法:
对于TCP协议中的客户端与服务端,它们之间做一次数据交互,需要4个字节流对象,客户端与服务端各一组输入输出字节流对象,分别用来向对方发出和接收数据。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @ClassName:Server
* @Description:服务端
*/
public class Server {
public static void main(String[] args) throws IOException {
//1. 指定服务端端口号,创建服务端
ServerSocket server=new ServerSocket(8888);
//2. 开启连接等待阻塞,直到有客户端建立连接,accept()方法才会解除阻塞退出,
// 并且返回一个与客户端连接的Socket(套接字)对象
Socket socket=server.accept();
//3. 发送数据到客户端
OutputStream out=socket.getOutputStream();
out.write("hello,this is server".getBytes());
//4. 从客户端接收数据
InputStream in=socket.getInputStream();
int length=socket.getReceiveBufferSize();//获取接收到的数据大小的真实字节长度
byte[] b=new byte[length];
in.read(b);
System.out.println(new String(b));
//6. 关闭资源,必须先关闭与客户端连接的套接字对象socket,然后关闭服务端server
socket.close();
server.close();
}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* @ClassName:Client
* @Description:客户端
*/
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
//1. 创建一个Socket对象,用于与服务端建立连接和通信,构造方法中传入服务端ip地址和端口
Socket socket=new Socket("127.0.0.1", 8888);
//2. 通过Socket对象来获取一个字节输入流对象,用于获取服务端发来的数据
InputStream in=socket.getInputStream();
//3. 通过Socket对象来获取一个字节输出流对象,用于向服务端发送数据
OutputStream out=socket.getOutputStream();
//4. 接收从服务端发来的数据
int length=socket.getReceiveBufferSize();//获取接收到的数据大小的真实字节长度
byte[] b=new byte[length];
in.read(b);
System.out.println(new String(b));
//5. 发送数据
out.write("hello,this is client".getBytes());
//6. 关闭资源
socket.close();
}
}
3. TCP网络程序实现文件上传(以图片为例):稍微思考,可以首先传输完整的文件名,然后再传输文件的字节流,实际上与通过字节流实现文件复制的原理相同。
/**
* @ClassName:UploadClient
* @Description:图片上传客户端
* 1. 建立连接
* 2. 首先获取本地图片文件的输入流读取数据
* 3. 通过Socket套接字对象获取输出到服务器的输出流
* 4. 输出完整文件名到服务器
* 5. 输出数据到服务端
* 6. 通过Socket套接字对象获取输入流接收服务端返回的数据
*/
public class UploadClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
String path="D:\\5a13818206778.jpg";
FileInputStream fin=new FileInputStream(path);
OutputStream out = socket.getOutputStream();
byte[] fb=new byte[1024];
int len=0;
while((len=fin.read(fb))!=-1){
out.write(fb,0,len);
}
//shutdownOutput()方法向服务端写终止序列,禁用当前套接字对象的OutputStream流
//之前写入的数据会全部被传输至服务端,后面再使用该套接字的OutputStream流将会抛出IOException
//使用该方法就表示当前所有要传输的数据都完成了,服务端在调用read方法读取数据时就不会进入等待阻塞
//如果没有数据传来就会直接返回
socket.shutdownOutput();
int length = socket.getReceiveBufferSize();
byte[] b = new byte[length];
InputStream in = socket.getInputStream();
in.read(b);
System.out.println(new String(b));
socket.close();
fin.close();
}
}
/**
* @ClassName:UploadServer
* @Description:图片上传服务端
* 1. 与客户端建立连接
* 2. 获取数据流
* 3. 存储在指定文件中
* 4. 向客户端发送响应,成功接收文件
*/
public class UploadServer {
public static void main(String[] args) throws IOException {
ServerSocket server=new ServerSocket(8888);
Socket socket=server.accept();
InputStream in=socket.getInputStream();
//创建文件夹
File dir=new File("D:\\develop");
if(!dir.exists()){
dir.mkdirs();
}
FileOutputStream fout=new FileOutputStream(dir+"\\upload.jpg");
int length=0;
byte[] b=new byte[1024];
while((length=in.read(b))!=-1){
fout.write(b,0,length);
}
OutputStream out=socket.getOutputStream();
out.write("success".getBytes());
socket.close();
server.close();
}
}
4. TCP网络程序加多线程实现一个简单的群聊应用:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class Client {
private Socket socket;
public Client()throws Exception {
socket=new Socket("localhost", 8088);
}
public void start()throws IOException{
OutputStream out=socket.getOutputStream();
OutputStreamWriter osw=new OutputStreamWriter(out, "UTF-8");
PrintWriter pw=new PrintWriter(osw,true);
Scanner scan=new Scanner(System.in);
String str=null;
ServerHandler handle=new ServerHandler();
Thread t=new Thread(handle);
t.start();
while(true){
str=scan.nextLine();
pw.println(str);
}
}
public static void main(String[] args) {
try{
Client client=new Client();
client.start();
}catch(Exception e){
e.printStackTrace();
}
}
private class ServerHandler implements Runnable{
public void run() {
try{
InputStream in=socket.getInputStream();
InputStreamReader isr=new InputStreamReader(in,"UTF-8");
BufferedReader br=new BufferedReader(isr);
String message=null;
while((message=br.readLine())!=null){
System.out.println(message);
}
}catch(Exception e){
}
}
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class Server {
/*
* java.net.ServerSocket
* 运行在服务端的ServerSocket有两个作用
* 申请服务端口,客户端通过该端口与服务端建立连接
* 监听该接口,一旦客户端通过该端口连接后会创建一个socket实例与该客户端通讯
*/
private ServerSocket server;
//保存所有客户端输出流的集合
private List<PrintWriter> allout;
public Server()throws IOException {
/*
* 创建ServerSocket的同时需要申请服务端口
* 这个端口不能与其他使用TCP协议的应用程序冲突,否则抛出异常
*/
server=new ServerSocket(8088);
allout=new ArrayList<PrintWriter>();
}
//向客户端输出流集合中添加元素
private synchronized void addout(PrintWriter pw){
allout.add(pw);
}
//向客户端输出流集合中删除元素
private synchronized void removeout(PrintWriter pw){
allout.remove(pw);
}
//遍历客户端输出流集合,将给定消息通过遍历集合将消息发送给每一个客户端
private synchronized void sendmessage(String message){
for(PrintWriter pw:allout){
pw.println(message);
}
}
//添加synchronized保证这三个方法互斥
public void start()throws IOException{
/*
* ServerSocket提供Socket accept( )方法
* 用于监听打开的服务端口,一旦一个客户端通过该端口请求时,就会创建一个
* Socket并返回,通过该Socket就可以与客户端进行交互
* 该方法是一个阻塞方法,直到客户端链接才会有返回值
*/
while(true){
System.out.println("等待客户端链接......");
Socket socket=server.accept();
System.out.println("一个客户端已链接");
/*
* 使用线程来解决多个客户端链接服务端的问题
*/
ClientHandle handle=new ClientHandle(socket);
Thread t=new Thread(handle);
t.start();
}
}
public static void main(String[] args) {
try{
Server server=new Server();
server.start();
}catch(Exception e){
e.printStackTrace();
}
}
/**
* 该类是一个线程的任务,负责与指定客户端进行交互
*
*/
private class ClientHandle implements Runnable{
/*
* 当前线程需要处理的针对指定客户端的socket
*/
private Socket socket;
private String host;//该用户ip地址信息
public ClientHandle(Socket socket) {
this.socket=socket;
//获取远程计算机地址信息
InetAddress address=socket.getInetAddress();
//获取ip地址
host=address.getHostAddress();
}
public void run() {
PrintWriter pw=null;
try{
OutputStream out=socket.getOutputStream();
OutputStreamWriter osw=new OutputStreamWriter(out, "UTF-8");
pw=new PrintWriter(osw,true);
addout(pw);
/*
* InputStream getInputStream( )
* Socket提供的该方法可以获取一个输入流,通过该输入流
* 可以读取远端计算机发送过来的数据
*/
System.out.println(host+"上线了");
InputStream in=socket.getInputStream();
InputStreamReader isr=new InputStreamReader(in,"UTF-8");
BufferedReader br=new BufferedReader(isr);
String message=null;
while((message=br.readLine())!=null){
/*
* 使用br.readLine()方法读取远端计算机发送过来的数据时,
* 由于操作系统可能不同
* 断开连接时的反应也不相同,当linux的客户端断开连接时会返回null
* windos客户端断开时会抛出异常
*/
sendmessage(host+"上线"+",当前在线人数共"+allout.size());
sendmessage("已收到来自于"+host+"的"+message);
}
}catch(IOException e){
e.printStackTrace();
}finally {
//客户端断开链接后处理
removeout(pw);
System.out.println(host+"下线了");
try{
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
}