什么是网络编程?
- 可以让设备中的程序与网络上其他设备中的程序进行数据交互(实现网络通信的)
java.net.*包下提供了网络编程的解决方案
基本的通信架构
- 基本的通信架构有2种形式:CS架构(Client客户端/Server服务端)、BS架构(Browser浏览器/Server服务端)
无论是CS架构,还是BS架构的软件都必须依赖网络编程!
网络通信的关键三要素
IP、端口、协议
IP地址
- IP(Internet Protocol):全称”互联网协议地址”是分配给上网设备的唯一标志
- IP地址有两种形式:IPV4、IPV6
IPV4地址
IPv6地址
- IPv6:共128位,号称可以为地球每一粒沙子编号
- IPV6分成8段表示,每段每四位编码成一个十六进制位表示,数之间用冒号(:)分开
公网IP,内网IP
- 公网IP:是可以连接互联网的IP地址:内网IP:也叫局域网IP,只能组织机构内部使用
- 192.168.开头的就是常见的局域网地址,范围即为192.168.0.0--192.168.255.255,专门为组织机构内部使用
特殊IP地址:
- 127.0.0.1、localhost:代表本机IP,只会寻找当前所在的主机
IP常用命令:
- ipconfig:查看本机IP地址
- ping IP地址:检查网络是否连通
InetAddress类
- 代表IP地址
InetAddress的常用方法
端口
- 标记正在计算机设备上运行的应用程序的,被规定为一个16 位的二进制,范围是0~65535
分类
- 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)
- 注册端口:1024~49151,分配给用户进程或某些应用程序
- 动态端口:49152到65535,之所以称为动态端口,是因为它 一般不固定分配某种进程,而是动态分配
注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错
通信协议
- 网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议
开放式网络互联标准:0SI网络参考模型
- OSI网络参考模型:全球网络互联标准
- TCP/IP网络模型:事实上的国际标准
传输层的2个通信协议
UDP(User Dataqram Protocol):用户数据报协议;TCP(Transmission ControlProtocol):传输控制协议。
传输层的2个通信协议
- UDP(User Dataqram Protocol):用户数据报协议
- TCP(Transmission ControlProtocol):传输控制协议
UDP协议
- 特点:无连接、不可靠通信
- 不事先建立连接,数据按照包发,一包数据包含:自己的IP、程序端口,目的地IP、程序端口和数据(限制在64KB内)等
- 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的
TCP协议
- 特点:面向连接、可靠通信
- TCP的最终目的:要保证在不可靠的信道上实现可靠的传输
- TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接
三次握手建立连接
三次握手:客户端发消息,服务器端确定客户端发消息没问题,服务器端回消息,客户端确认服务器端收发消息没问题,客户端再发消息,服务器端确认客户端收消息也没问题,这就是三次握手建立可靠连接
传输数据时,客户端给服务端发送一个消息,服务端一定要回一个消息确认收到此消息,否则客户端将认为服务端未收到消息,继续发送此消息
四次握手断开连接
首先,客户端发送断开连接请求,此时服务端可能还没接收完数据,会返回一个响应让客户端再等一下,等服务端接收完数据,再返回一个相应表示可以断开连接了,但是在返回这个相应的时候极大可能客户端同时也发送了一些消息,此时客户端接收完这些消息之后,就可以发出正式确认断开连接了
UDP通信
需要用到DatagramSocket类和DatagramPacket类
DatagramSocket:用于创建客户端、服务端
DatagramPacket:创建数据包
客户端实现步骤
- 创建DatagramSocket对象(客户端对象)
- 创建DatagramPacket对象封装需要发送的数据(数据包对象)
- 使用DatagramSocket对象的send方法,传入DatagramPacket对象
- 释放资源
服务端实现步骤
- 创建DatagramSocket对象并指定端口(服务端对象)
- 创建DatagramPacket对象接收数据(数据包对象)
- 使用DatagramSocket对象的receive方法,传入DatagramPacket对象
- 释放资源
代码演示:
客户端:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class Client2 {
public static void main(String[] args) throws Exception{
//1、创建客户端对象
DatagramSocket socket = new DatagramSocket();
Scanner sc = new Scanner(System.in);
//2、创建数据包对象封装要发出去的数据
/*DatagramPacket
参数一:封装要发出去的数据
参数二:发送出去的数据大小(字节个数)
参数三:服务端的IP地址(找到服务端主机)
参数四:服务端程序的端口
*/
while (true) {
System.out.print("请说:");
String msg = sc.nextLine();
byte[] bytes = (msg).getBytes();
DatagramPacket packet = new DatagramPacket(bytes,bytes.length, InetAddress.getLocalHost(),9999);
//3、正式发送数据包出去
socket.send(packet);
if(msg.equals("exit")){
break;
}
}
System.out.println("客户端数据发送完毕!");
socket.close(); //释放资源
}
}
服务端:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class Server2 {
public static void main(String[] args) throws Exception{
System.out.println("-----服务端启动-----");
//1、创建一个服务端对象
DatagramSocket socket = new DatagramSocket(9999);
while (true) {
//2、创建一个数据包对象,用来接收数据
byte[] buffer = new byte[1024 * 64]; //64kb,一包数据不会超过64kb
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
//3、使用数据包接收客户端发送的数据
socket.receive(packet);
//4、从字节数组中把接收的数据直接打印出来
//接收多少倒出多少
int len = packet.getLength();
String rs = new String(buffer,0,len);
if(rs.equals("exit")){
System.out.println("客户端" + packet.getAddress().getHostAddress() + ":" + packet.getPort() + "下线了");
System.out.println("------------------------------");
socket.close();
break;
}
System.out.println(rs);
//客户端的IP地址+客户端的端口
System.out.println(packet.getAddress().getHostAddress() + ":" + packet.getPort());
System.out.println("------------------------------");
}
}
}
注意要先启动服务端再启动客户端
运行结果:
TCP通信
- 特点:面向连接、可靠通信,
- 通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端
- Java提供了一个java.net.Socket类来实现TCP通信
TCP通信之-客户端开发
- 客户端程序就是通过java.net包下的Socket类来实现的
客户端实现步骤
① 创建客户端的Socket对象,请求与服务端的连接
②使用socket对象调用qetOutputStream()方法得到字节输出流
③ 使用字节输出流完成数据的发送
④ 释放资源:关闭socket管道
TCP通信-服务端程序的开发
- 服务端是通过java.net包下的Serversocket类来实现的
服务端实现步骤
① 创建ServerSocket对象,注册服务端端口。
② 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象
③ 通过Socket对象调用getinputStream()方法得到字节输入流、完成数据的接收
④ 释放资源:关闭socket管道
代码演示:
客户端:
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws Exception {
//1、创建Socket对象,并同时请求与服务端程序连接
Socket socket = new Socket("127.0.0.1",8888);
//从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
OutputStream os = socket.getOutputStream();
//3、把字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请说:");
String msg = sc.nextLine();
if(msg.equals("exit")){
break;
}
//4、发送数据
dos.writeUTF(msg);
}
dos.close();
socket.close();
}
}
服务端:一发一收
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws Exception {
//1、创建ServerSocket的对象,同时未服务端注册端口
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = null;
DataInputStream dis = null;
//2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
socket = serverSocket.accept();
//3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
//4、把原始的字节输入流包装成数据输入流
dis = new DataInputStream(is);
//5、使用数据输入流读取客户端发送过来的消息
while (true) {
try {
String rs = dis.readUTF();
System.out.println(rs);
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了");
dis.close();
socket.close();
serverSocket.close();
break;
}
System.out.println(socket.getRemoteSocketAddress());
System.out.println("----------------------------------");
}
}
}
运行结果:
服务端:多发多收
import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server2 {
public static void main(String[] args) throws Exception {
//1、创建ServerSocket的对象,同时未服务端注册端口
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = null;
DataInputStream dis = null;
while (true) {
//2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
socket = serverSocket.accept();
//3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理
new ServerReaderThread(socket).start();
}
}
}
ServerReaderThread线程
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true) {
try {
String rs = dis.readUTF();
System.out.println(rs);
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了");
System.out.println("----------------------------------");
dis.close();
socket.close();
break;
}
System.out.println(socket.getRemoteSocketAddress());
System.out.println("----------------------------------");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
TCP:端口转发
群聊案例:
客户端:
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws Exception{
//1、创建Socket对象,并同时请求与服务端程序连接
Socket socket = new Socket("127.0.0.1",8888);
//启动客户端接收消息线程
new ClientReaderThread(socket).start();
//从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
OutputStream os = socket.getOutputStream();
//3、把字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
String msg = sc.nextLine();
if(msg.equals("exit")){
break;
}
//4、发送数据
dos.writeUTF(msg);
dos.flush();
}
dos.close();
socket.close();
}
}
客户端读线程类:
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;
public class ClientReaderThread extends Thread{
private Socket socket;
public ClientReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true) {
try {
String msg = dis.readUTF();
System.out.println(msg);
} catch (Exception e) {
System.out.println("我离线了");
System.out.println("----------------------------------");
dis.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务端:
import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
public class Server {
public static ArrayList<Socket> sockets = new ArrayList<>(); //客户端群,记录所有客户端
public static void main(String[] args) throws Exception {
//1、创建ServerSocket的对象,同时未服务端注册端口
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = null;
DataInputStream dis = null;
System.out.println("------------服务端启动------------");
while (true) {
//2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
socket = serverSocket.accept();
sockets.add(socket); //每连接到一个客户端就加入客户端群(集合)
//3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理
new ServerReaderThread(socket).start();
}
}
}
服务端读线程类:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream(); //字节输入流
DataInputStream dis = new DataInputStream(is); //打包为数据输入流
while (true) {
try {
System.out.println("----------------------------------");
String msg = dis.readUTF(); //收到消息
System.out.println(socket.getRemoteSocketAddress() + ":" + msg);
SendMsgToAll(socket.getRemoteSocketAddress() + ":" + msg);
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了");
Server.sockets.remove(socket);
System.out.println("----------------------------------");
dis.close();
socket.close();
break;
}
System.out.println("----------------------------------");
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void SendMsgToAll(String msg) throws Exception{
for (Socket socket : Server.sockets) { //遍历群聊里每位成员
OutputStream os = socket.getOutputStream(); //从每位成员的通信管道中得到一个字节输出流
DataOutputStream dos = new DataOutputStream(os); //包装成数据输出流
dos.writeUTF(msg); //发出数据
dos.flush(); //刷新
}
}
}
运行结果:
BS架构的基本原理
将web浏览器作为客户端,统一了客户端
注意:服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据
HTTP协议规定:响应给浏览器的数据格式必须满足如下格式
使用线程池进行优化
代码演示:
服务端:
package com.zeyu.tcp;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class browser_Server {
public static void main(String[] args) throws Exception {
System.out.println("------------服务器启动------------");
//1、创建ServerSocket的对象,同时为服务器注册端口
ServerSocket serverSocket = new ServerSocket(8080);
//创建出一个线程池,负责处理通信管道的任务
ThreadPoolExecutor poll = new ThreadPoolExecutor(16 * 2, 16 * 2, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
while (true) {
//2、使用serverSocket对象,调用一个accept方法,等待客户端的连接
Socket socket = serverSocket.accept();
System.out.println("有人访问" + socket.getRemoteSocketAddress());
//3、把这个客户端对应的socket通信管道,交给一个独立的线程处理
poll.execute(new ServerRunnable(socket));
}
}
}
ServerRunnable
package com.zeyu.tcp;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
public class ServerRunnable implements Runnable{
private Socket socket;
public ServerRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
OutputStream os = socket.getOutputStream();
PrintStream pos = new PrintStream(os);
pos.println("HTTP/1.1 200 OK");
pos.println("Content-Type:text/html;charset=UTF-8");
pos.println();
pos.println("<div style='color:blue;font-size:60px;text-align:center'>世界都承载在你小小的肩膀上</div>");
pos.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端(浏览器):
运行结果: