1、什么是网络编程
网络在通信协议下,不同计算机上运行的程序,进行的数据传输
应用场景:即时通信,网游对战,金融证券,国际贸易,邮件等等
不管什么场景,本质上都是计算机跟计算机之间通过网络进行数据传输
java中可以使用java.net包下的技术轻松开发出常见的网络应用程序
2、常见的软件架构
C / S :Client / Server
在用户本地需要下载安装客户端程序,在远程有一个服务器端程序
C / S的优缺点:
1、画面可以做的非常精美,用户体验好
2、需要开发客户端,也需要开发服务端
3、用户需要下载更新的时候太麻烦
B / S : Browser / Server
只需要一个浏览器,用户通过不同的地址,客户访问不同的服务器
B / S的优缺点:
1、不需要开发客户端
2、用户不需要下载,打开浏览器就能使用
3、如果应用过大,用户体验受到影响
注意:无论什么架构,都有各自的优缺点,具体看实际情况定
3、网络编程三要素
- ip
- 设备在网络中的地址
- 端口号
- 应用程序在设备中的唯一识别
- 协议
- 数据在网络中传输的规则,常见的协议有TCP,http,ftp等
4、InetAddress类
ip地址的代表类是谁
InetAddress
如何获取本机ip对象
public static InetAddress getLocalHost()
如何判断与该ip地址对象是否互通
public boolean isReachable(int timeout)
public class myAddress {
public static void main(String[] args) throws UnknownHostException {
/**
* static InetAddress getByName(String host) 确定主机名称的ip地址,主机名可以是机器名,也可以是ip地址
* String getHostName() 获取此ip地址的主机名
* String getHostAddress 返回文本显示中的ip地址字符串
*/
// 1.获取InetAddress对象
InetAddress address = InetAddress.getByName("DESKTOP-1G6DPV3");
System.out.println(address);
// 2.获取名称
String hostName = address.getHostName();
System.out.println(hostName);
// 3.获取ip
String hostAddress = address.getHostAddress();
System.out.println(hostAddress);
}
}
5、端口号
标识正在计算机设备上运行的进程,被规定为一个16位的二进制,范围0~65535
端口类型
- 周知端口:0~1023,被预先定义的知名应用占用(如:http占用80,FTP占用21)
- 注册端口:1024~49151,分配给用户进程或者某些应用程序(如:tomcat占用8080,MySQL占用3306)
- 动态端口:49152~65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配
注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错
6、协议
协议是连接和通信数据的规则
网络通信协议有两套参考模型
- OSI参考模型 : 7层
- TCP/IP参考模型:4层
传输层常见的两个协议
- TCP:传输控制协议
- 使用tcp协议必须建立连接,他是一种面向对象的可靠协议
- 采用三次握手,四次挥手,所以可靠
- 在连接中可进行大数据量传输
- 连接、发送数据都需要确认,且传输完毕后,还需要释放自己建立的连接,通信效率低
- UDP:用户数据报协议
- udp是无连接不可靠协议
- 将数据源ip、目的地ip和端口号封装成数据包,不需要建立连接
- 每个包再64kb内
- 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
- 可以广播发送,发送数据结束时无需释放资源,开销小,速度快
7、UDP通信
DatagramPacket:数据包对象
构造器 | 说明 |
---|---|
public DatagramPacket(byte[] buf,int length,InetAddress address,int port) | 创建发送对象 buf:要发送的内容,字节数组 length:要发送内容的字节长度 address:接收端的IP地址对象 port:接受收的端口号 |
public DatagramPacket(byte[] buf,int length) | 创建接收端的数据包对象 buf:用来存储接收的内容 length:能够接收内容的长度 |
DatagramSocket:发送端和接收端对象
构造器 | 说明 |
---|---|
public DatagramSocket() | 创建发送端的Socket对象,系统会随机分配一个端口号 |
public DatagramSocket(int port) | 创建接收端的Socket对象并指定端口号 |
DatagramSocket类成员方法
方法 | 说明 |
---|---|
public void send(DatagramPacket dp) | 发送数据包 |
public void receive(DatagramPacket p) | 接收数据包 |
7.1、一收一发
需求
- 客户端实现步骤
- 创建DatagramSocket对象(发送端对象)
- 创建DatagramPacket对象封装需要发送的数据(数据包对象)
- 使用DatagramSocket对象的send方法传入DatagramPack对象
- 释放资源
- 接收端实现步骤
- 创建DatagramSocket对象并指定端口(接收端对象)
- 创建DatagramPacket对象接收数据(数据包对象)
- 使用DatagramSocket对象的receive方法传入DatagramPacket对象
- 释放资源
代码案例
发送端
package com.lwj.address.demo02; import java.io.IOException; import java.net.*; /** * 学会udp通信, 需求 1发1收 * 客户端:发送端 */ public class ClientDemo01 { public static void main(String[] args) throws IOException { //创建发送端对象 DatagramSocket socket = new DatagramSocket(); //创建数据包对象 byte[] buffer = "我是客户端,请问在吗".getBytes(); DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),6666); //发送数据包 socket.send(packet); //释放资源 socket.close(); } }
接收端
package com.lwj.address.demo02; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; /** * */ public class ServerDemo01 { public static void main(String[] args) throws IOException { //创建接收端对象,接收端必须注册端口 DatagramSocket socket = new DatagramSocket(6666); // 创建数据包对象 byte[] buffer = new byte[64 * 1024]; DatagramPacket packet = new DatagramPacket(buffer,buffer.length); //接收数据包 socket.receive(packet); //得到读取的字节数量 int len = packet.getLength(); System.out.println("读取的字节数为" + len); //把数据转换出来 String rs = new String(buffer,0, len); System.out.println("收到客户端发送的消息" + rs); //拿发送端的ip地址和端口 System.out.println(packet.getSocketAddress()); System.out.println(packet.getPort()); } }
7.2、UDP如何实现广播
- 使用广播地址:255.255.255.255
- 具体操作
- 发送端发送的数据包的目的地写广播地址且指定端口(255.255.255.255,9999)
- 本机所在网段的其他主机和程序只要注册对应端口就可以收到消息了(9999)
7.3、UDP如何实现组播
- 使用组播地址:224.0.0.0~239.255.255.55
- 具体操作:
- 发送端的数据包的目的是组播IP(例如:224.0.1.1,端口:9999)
- 接收端必须绑定该组播IP(224.0.1.1),端口还要注册发送端的目的端口,这样可以即可接收该组播消息
- DatagramSocket的子类MulticastSocket可以绑定组播ip
代码演示
发送端
public class ClientDemo04 { public static void main(String[] args) throws IOException { DatagramSocket socket = new DatagramSocket(); Scanner scanner = new Scanner(System.in); while (true){ System.out.println("请输入你的信息"); String s = scanner.nextLine(); if ("exit".equals(s)){ break; } byte[] buffer = s.getBytes(); DatagramPacket packet = new DatagramPacket(buffer,buffer.length,InetAddress.getByName("225.1.1.1"),8989); socket.send(packet); } socket.close(); } }
服务端
public class ServerDemo04 { public static void main(String[] args) throws IOException { System.out.println("=====服务端启动====="); // 1、创建接收端对象,注册端口 MulticastSocket socket = new MulticastSocket(8989); // 2、注册组播地址 socket.joinGroup(new InetSocketAddress(InetAddress.getByName("225.1.1.1"),8989), NetworkInterface.getByInetAddress(InetAddress.getLocalHost())); byte[] buffer = new byte[64*1024]; DatagramPacket packet = new DatagramPacket(buffer,buffer.length); while (true){ socket.receive(packet); int len = packet.getLength(); //把数据转换出来 String rs = new String(buffer,0, len); System.out.println("收到客户端发送的消息:" + rs); } } }
8、TCP通信
客户端
- TCP的通信客户端代代表类是谁?
- Socker类
- public Socket(String host,int port)
- TCP通信如何使用Socket管道发送、接收数据
- OutputStream getOutputStream() :获取字节输出流对象(发)
- InputStream getInputStream() :获取字节输入流对象(收)
构造器 | 说明 |
---|---|
public Socket(String host,int port) | 创建发送端的Socket对象与服务端连接,参数为服务端程序的ip和端口 |
方法 | 说明 |
---|---|
OutputStream getOutputStream() | 获取字节输入流对象 |
InputStream getInputStream() | 获取字节输入流对象 |
服务端
- TCP通信服务端用的代表类?
- ServerSocket类,注册端口
- 调用accept()方法阻塞等待接收客户端连接,得到Socket对象
- TCP通信的基本原理
- 客户端怎么发,服务端怎么收
- 客户端如果没有发消息,服务端会进入阻塞等待
- Socket一方关闭或者出现异常、对方Socket也会失效或者出错
构造器 | 说明 |
---|---|
public ServerSocket(int port) | 注册服务端端口 |
方法 | 说明 |
---|---|
public Socket accept() | 等待接收客户端的Socket通信连接 连接成功返回Socket对象与客户端建立端到端通信 |
8.1、客户端编写
需求:客户端实现步骤
- 创建客户端Socket对象,请求与服务端的连接
- 使用socket对象调用getOutputStream()方法得到字节输入流
- 使用字节输出流完成数据的发送
- 释放刷新资源
代码示例
public class ClientDemo01 {
public static void main(String[] args) throws IOException {
//1、创建Socket对象,请求与服务器的连接
Socket socket = new Socket("127.0.0.1",9999);
//2、从socket通信管道得到一个字节输出流,负责写数据进去
OutputStream os = socket.getOutputStream();
//3、把低级字节输出流交给打印流
PrintStream ps = new PrintStream(os);
//4、发送消息
ps.print("你好");
//5、刷新
ps.flush();
}
}
8.2、服务端编写
需求:服务端实现步骤
- 创建ServerSocket对象,注册服务端端口
- 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象
代码示例
public class ServerDemo01 {
public static void main(String[] args) throws IOException {
System.out.println("服务端启动....");
//1、注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//2、开始等待接收客户端的连接请求
Socket accept = serverSocket.accept();
//3、从socket通信管道中得到字节输入流
InputStream inputStream = accept.getInputStream();
//4、读取消息
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line;
if ((line = br.readLine()) != null) {
System.out.println(accept.getRemoteSocketAddress() + "说了" + line);
}
}
}
8.3、TCP多发多收通信
需求:使用TCP通信方式:多发多收
- 可以使用循环控制服务端收完消息继续等待接收下一个消息
- 客户端也可以使用死循环等待用户不断输入消息
- 客户端一但输入exit,则关闭客户端程序,并释放资源
客户端编写
public class ClientDemo02 {
public static void main(String[] args) throws IOException {
//1、创建Socket对象,请求与服务器的连接
Socket socket = new Socket("127.0.0.1",9999);
Scanner scanner = new Scanner(System.in);
//2、从socket通信管道得到一个字节输出流,负责写数据进去
OutputStream os = socket.getOutputStream();
//3、把低级字节输出流交给打印流
PrintStream ps = new PrintStream(os);
while (true){
String mess = scanner.nextLine();
if (mess.equals("exit")){
socket.close();
break;
}
//4、发送消息
ps.println(mess);
//5、刷新
ps.flush();
}
}
}
服务端
public class ServerDemo02 {
public static void main(String[] args) throws IOException {
System.out.println("服务端启动....");
//1、注册端口
ServerSocket serverSocket = new ServerSocket(9999);
//2、开始等待接收客户端的连接请求
Socket accept = serverSocket.accept();
//3、从socket通信管道中得到字节输入流
InputStream inputStream = accept.getInputStream();
//4、读取消息
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = br.readLine()) != null) {
System.out.println(accept.getRemoteSocketAddress() + "说了" + line);
}
}
}
8.4、同时接收多个客户端消息
之前我们的通信是否可以同时与多个客户端通信,为什么
- 不可以
- 单线程每次只能处理一个客户端的Socket通信
如何才可以让服务i端处理多个客户端的通信需求
- 引入多线程
客户端编写
public class ClientDemo03 {
public static void main(String[] args) throws IOException {
//1、创建Socket对象,请求与服务器的连接
Socket socket = new Socket("127.0.0.1",9999);
Scanner scanner = new Scanner(System.in);
//2、从socket通信管道得到一个字节输出流,负责写数据进去
OutputStream os = socket.getOutputStream();
//3、把低级字节输出流交给打印流
PrintStream ps = new PrintStream(os);
while (true){
String mess = scanner.nextLine();
if (mess.equals("exit")){
socket.close();
break;
}
//4、发送消息
ps.println(mess);
//5、刷新
ps.flush();
}
}
}
线程类编写
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//从socker管道获取字节输入流读取数据
InputStream inputStream = socket.getInputStream();
//读取消息
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + " 说了:" + line);
}
System.out.println("有人下线了" + socket.getRemoteSocketAddress());
} catch (IOException e) {
System.out.println("有人非正常下线了" + socket.getRemoteSocketAddress());
}
}
}
服务端编写
package com.lwj.address.demo07;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo03 {
public static void main(String[] args) throws IOException {
System.out.println("服务端启动....");
ServerSocket serverSocket = new ServerSocket(9999);
while (true){
//开始等待接收客户端socket连接请求
Socket socket = serverSocket.accept();
System.out.println("有人上线了" + socket.getRemoteSocketAddress());
//创建线程负责处理socket信息
new ServerReaderThread(socket).start();
}
}
}
8.5、使用线程池优化
8.4的通信架构存在什么问题?
- 客户端与服务端的线程模型是:N-N的关系
- 客户端并发越多,系统瘫痪的越快
优化
- 客户端编写
public class ClientDemo {
public static void main(String[] args) throws IOException {
//1、创建Socket对象,请求与服务器的连接
Socket socket = new Socket("127.0.0.1",7777);
Scanner scanner = new Scanner(System.in);
//2、从socket通信管道得到一个字节输出流,负责写数据进去
OutputStream os = socket.getOutputStream();
//3、把低级字节输出流交给打印流
PrintStream ps = new PrintStream(os);
while (true){
String mess = scanner.nextLine();
if (mess.equals("exit")){
socket.close();
break;
}
//4、发送消息
ps.println(mess);
//5、刷新
ps.flush();
}
}
}
- 服务端
public class ServerDemo {
//定义静态的线程池对象
public static ExecutorService pool = new ThreadPoolExecutor(2,3,5,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) throws IOException {
System.out.println("服务端启动...");
//1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
while (true){
// 2、开始等待接收客户端的socket连接请求
Socket socket = serverSocket.accept();
System.out.println("有人上线了" + socket.getRemoteSocketAddress());
//3、把当前客户端的socket做出任务对象交给线程池处理
pool.execute(new ServerReaderRunnable(socket));
}
}
}
- 线程实现类
/**
* 读取负责socket的消息
*/
public class ServerReaderRunnable implements Runnable {
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//从socket管道获取字节输入流读取数据
InputStream inputStream = socket.getInputStream();
//读取消息
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + " 说了:" + line);
}
System.out.println("有人下线了" + socket.getRemoteSocketAddress());
} catch (IOException e) {
System.out.println("有人非正常下线了" + socket.getRemoteSocketAddress());
}
}
}
9、TCP实战案例----即时通信
即时通信是什么含义,要实现怎么样的设计?
- 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到
- 之前我们的消息都是发给服务端的
- 即时通信需要我们进行端口转发的设计思想
即时通信-端口转发
9.1、实现群聊
客户端编写
public class ClientDemography {
public static void main(String[] args) throws IOException {
//创建Socket对象,请求与服务器的连接
Socket socket = new Socket("127.0.0.1",7777);
Scanner scanner = new Scanner(System.in);
//为当前这个客户端socket分配一个独立的读线程,负责读取服务器转发过来的其他客户端的信息
new ClientReaderThreadDemo(socket).start();
//从socket通信管道得到一个字节输入流,负责写数据进去
OutputStream outputStream = socket.getOutputStream();
//把低级字节输出流给打印流,负责写数据进去
PrintStream ps = new PrintStream(outputStream);
while (true){
String mess = scanner.nextLine();
if (mess.equals("exit")){
socket.close();
break;
}
//4、发送消息
ps.println(mess);
//5、刷新
ps.flush();
}
}
}
客户端接收线程类
public class ClientReaderThreadDemo extends Thread{
private Socket socket;
public ClientReaderThreadDemo(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//从socket管道获取字节输入流读取数据
InputStream inputStream = socket.getInputStream();
//读取数据
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + "说了" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端编写
public class ServerDemography {
//定义一个静态集合,存储所有在线的socket管道
public static List<Socket> socketList = new ArrayList<>();
public static void main(String[] args) throws IOException {
System.out.println("服务端启动....");
ServerSocket serverSocket = new ServerSocket(7777);
while (true){
//开始等待接收客户端socket连接请求
Socket socket = serverSocket.accept();
System.out.println("有人上线了" + socket.getRemoteSocketAddress());
//存进集合
socketList.add(socket);
//创建线程负责处理socket信息
new ServerReaderThreadDemo(socket).start();
}
}
}
服务端线程类编写
public class ServerReaderThreadDemo extends Thread{
private Socket socket;
public ServerReaderThreadDemo(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//从socket管道获取字节输入流读取数据
InputStream inputStream = socket.getInputStream();
//读取数据
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + "说了" + line);
//将消息推送给当前所有在线socket,排除自己
senMsgToAll(line);
}
System.out.println("有人下线了" + socket.getRemoteSocketAddress());
} catch (IOException e) {
System.out.println("有人下线了" + socket.getRemoteSocketAddress());
}
}
private void senMsgToAll(String msg) throws IOException {
/**
* 1、遍历全部在线socket
* 2、排除自己的Socket,不应该发给自己
* 3、从这个socket得到一个字节输入流,包装成打印流写消息
*/
for (Socket newSocket : ServerDemography.socketList) {
if (newSocket != socket){
PrintStream ps = new PrintStream(newSocket.getOutputStream());
ps.println(msg);
ps.flush();
}
}
}
}
9.2、模拟BS系统
BS结构是什么样的,需要开发客户端嘛
- 浏览器访问服务端,不需要开发客户端,是由服务端直接响应网页数据的
- 所以我们只需要开发服务端,然后用本地浏览器测试
服务端编写
package com.lwj.address.demo10;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
public class BSServer {
//定义静态的线程池对象
public static ExecutorService pool = new ThreadPoolExecutor(2,3,5,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) throws IOException {
System.out.println("服务端启动...");
//1、注册端口
ServerSocket serverSocket = new ServerSocket(8080);
while (true){
// 2、开始等待接收客户端的socket连接请求
Socket socket = serverSocket.accept();
System.out.println("有人上线了" + socket.getRemoteSocketAddress());
//3、把当前客户端的socket做出任务对象交给线程池处理
pool.execute(new ServerReaderRunnable(socket));
}
}
}
服务端线程
package com.lwj.address.demo10;
import java.io.*;
import java.net.Socket;
/**
* 读取负责socket的消息
*/
public class ServerReaderRunnable implements Runnable {
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//响应一个网页数据
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("HTTP/1.1 200 0k");
ps.println("Content-Type:text/html;charset=UTF-8");
ps.println(); //换行
ps.println("<div style='color:green'>你好</div>");
ps.flush();
socket.close();
} catch (Exception e) {
System.out.println("有人非正常下线了" + socket.getRemoteSocketAddress());
}
}
}
运行结果