什么是网络编程?
- 可以让设备中的程序与网络上其他设备中的程序进行数据交互(实现网络通信的)
java.net.*包下提供了网络编程的解决方案!
基本的通信架构
- 基本的通信架构有2种形式:CS架构(Client客户端/Server服务端)BS架构(Browser浏览器/Server服务端)。
无论是CS架构,还是BS架构的软件都必须依赖网络编程!
网络通信三要素
IP地址
- IP(Internet Protocol):全称”互联网协议地址”,是分配给上网设备的唯一标志。
- IP地址有两种形式:IPV4、IPV6
自己电脑上的ip地址查看:cmd输入ipconfig
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地址,
- pingIP地址:检查网络是否连通
InetAddress
- 代表IP地址
端口号
端口
- 标记正在计算机设备上运行的应用程序的,被规定为一个16 位的二进制,范围是 0~65535。
分类
- 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)
- 注册端口:1024~49151,分配给用户进程或某些应用程序。
- 动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。
协议
- 网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。
开放式网络互联标准:0SI网络参考模型
- OSI网络参考模型:全球网络互联标准。
- TCP/IP网络模型:事实上的国际标准。
传输层的2个通信协议
- UDP(User Datagram Protocol):用户数据报协议;
- TCP(Transmission ControlProtocol):传输控制协议。
UDP协议 通信效率高! 语音通话 视频直播
- 特点:无连接、不可靠通信。
- 不事先建立连接,数据按照包发一包数据包含:自己的IP、程序端口,目的地IP、程序端口和数据(限制在64KB内)等。
- 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的。
TCP协议 通信效率相对不高! 网页、文件下载、支付
- 特点:面向连接、可靠通信。
- TCP的最终目的:要保证在不可靠的信道上实现可靠的传输。
- TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。
TCP协议:三次握手建立可靠连接(只有三次握手才能建立可靠连接)
可靠连接:确定通信双方,收发消息都是正常无问题的!(全双工)
传输数据会进行确认,以保证数据传输的可靠性
TCP协议:四次握手断开连接
目的:确保双方数据的收发都已经完成!
UDP通信-快速入门
UDP通信
- 特点:无连接、不可靠通信。
- 不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了。
- Java提供了一个java.net.Datagramsocket类来实现UDP通信。
DatagramSocket:用于创建客户端、服务端
DatagramPacket:创建数据包
一发一收:
//客户端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
/**
* 完成udp通信快速入门,实现一发一收
*/
public class Client {
public static void main(String[] args) throws Exception {
//1.创建客户端对象(发送韭菜出去的人)
DatagramSocket socket = new DatagramSocket();
//2.创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
/* public DatagramPacket(byte buf[], int length,
InetAddress address, int port)*/
//参数一:封装要发出去的数据调用getBytes将字符串转成字节数组
//参数二:发送出去的数据大小(字节个数)
//参数三:服务端的ip地址(找到服务端主机)跟别人连getByName
//参数四:服务端程序的端口
byte[] bytes = "我要变强壮".getBytes();//bytes代表我们要发出去的数据
DatagramPacket packet = new DatagramPacket(bytes, bytes.length,
InetAddress.getLocalHost(), 6666);
//3.开始正式发送这个数据包的数据出去了
socket.send(packet);
System.out.println("客户端数据发送完毕~");
//发完之后关闭这个通信管道
socket.close();//因为会占网卡资源往外推数据,所以要释放系统资源
}
}
//服务端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("---------服务端启动--------");
//1.创建一个服务端对象(创建一个接韭菜的人)
DatagramSocket socket = new DatagramSocket(6666);//一定要在服务器注册端口
//2.创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
byte[] buffer = new byte[1024 * 64];//一包数据是不会超过64kb的
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
//3.开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);//先启动服务端,因为现在没有数据给他接所以会在这等,一旦有数据过来收到了才会往下走
//4.从字节数组中,把接收到的数据直接打印出来(把字节数组扔进去,String把字符数组转成字符串的形式)
//实现接收多少就倒出多少
//获取本次数据包接收了多少数据,
int len = packet.getLength();
String rs = new String(buffer,0,len);//从0开始,读了多少就接多少
System.out.println(rs);
//packet.getAddress().getHostAddress()拿客户端的IP地址-》172.26.60.3
System.out.println(packet.getAddress().getHostAddress());//packet.getAddress()拿客户端的ip地址-》/172.26.60.3
//客户端的端口
System.out.println(packet.getPort());//52456因为客户端用的是无参的DatagramSocket(),系统会随机为客户端分配一个端口,当然也可以自己给他分配一个端口
//可以获取客户端ip和端口以后方便给客户端回复消息
socket.close();//客户端管道用完之后应该把资源释放
}
}
注意:先启动服务端在启动客户端
UDP通信-多发多收
//客户端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws Exception {
//1.创建客户端对象(发送韭菜出去的人)
DatagramSocket socket = new DatagramSocket();
//2.创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
/* public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发出去的数据调用getBytes将字符串转成字节数组
参数二:发送出去的数据大小(字节个数)
参数三:服务端的ip地址(找到服务端主机)跟别人连getByName
参数四:服务端程序的端口
*/
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入");
String msg = sc.nextLine();//让用户在这输入一行数据
//新增需求:一旦发现用户输入的是exit命令就退出客户端
if ("exit".equals(msg)){
System.out.println("欢迎下次光临!退出成功!");
socket.close();
break;//跳出死循环——》关闭通信端 释放资源
}
//将msg消息转成字节数组的形式->再把这个字节数组封装成数据包->再把数据包发出去
byte[] bytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length,
InetAddress.getLocalHost(), 6666);
//3.开始正式发送这个数据包的数据出去了
socket.send(packet);
}
}
}
//服务端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("---------服务端启动--------");
//1.创建一个服务端对象(创建一个接韭菜的人)
DatagramSocket socket = new DatagramSocket(6666);//一定要在服务器注册端口
//2.创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
byte[] buffer = new byte[1024 * 64];//一包数据是不会超过64kb的
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
while (true) {
//3.开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);//先启动服务端,因为现在没有数据给他接所以会在这等,一旦有数据过来收到了才会往下走
//4.从字节数组中,把接收到的数据直接打印出来(把字节数组扔进去,String把字符数组转成字符串的形式)
//实现接收多少就倒出多少
//获取本次数据包接收了多少数据,
int len = packet.getLength();
String rs = new String(buffer,0,len);//从0开始,读了多少就接多少
System.out.println(rs);
//packet.getAddress().getHostAddress()拿客户端的IP地址-》172.26.60.3
System.out.println(packet.getAddress().getHostAddress());//packet.getAddress()拿客户端的ip地址-》/172.26.60.3
//客户端的端口
System.out.println(packet.getPort());//52456因为客户端用的是无参的DatagramSocket(),系统会随机为客户端分配一个端口,当然也可以自己给他分配一个端口
System.out.println("--------------------------------------------");
//可以获取客户端ip和端口以后方便给客户端回复消息
//socket.close();//客户端管道用完之后应该把资源释放
}
}
}
1.UDP的接收端为什么可以接收很多发送端的消息?
- 接收端只负责接收数据包,无所谓是哪个发送端的数据包
服务端同时接收多个客户端消息:IDEA默认一个程序只能启动一次,需要做一个配置才能实现多开,点击编辑配置,就用默认选中的客户端,然后点击修改选项的菜单,选择允许多开实例,最后应用。此时多开还会出现错误——绑定异常,(端口已经被使用被占用:一个电脑中不能出现两个应用程序的端口一样,直接把端口去掉,默认分配端口)
TCP通信-快速入门
TCP通信(非常适合做消息通信、网页浏览、文件下载等对通信质量要求较高的业务场景)
- 特点:面向连接、可靠通信
- 通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
- Java提供了一个java.net.socket类来实现TCP通信。
TCP通信之-客户端开发
- 客户端程序就是通过java.net包下的Socket类来实现的。
TCP通信-服务端程序的开发
- 服务端是通过java.net包下的Serversocket类来实现的。
//客户端
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
/**
* 目标:完成TCP通信——客户端开发:实现一发一收
*/
public class Client {
public static void main(String[] args) throws Exception {
//1.创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);//客户端发起连接!
/*这一行代码就会根据地址找服务器主机,根据端口号找程序,就会请求与他建立连接,
这边就会得到一个socket对象,服务端也会得到一个socket对象 ,两个socket对象就会建立一个端到端的通信管道,就连通了
连接一打通服务端、客户端都会往下跑,一、如果是服务端跑的快,服务端会立即跑到5.等待客户端发消息过来,
假如客户端消息还没过来,服务端会在这死等,会阻塞等待客户端发消息过来,等客户端执行到4.就把消息发送到服务端,就收到了
二、如果客户端发的快,服务端还没跑到5.,此时服务端会在底层先把发来的消息缓存着,
等服务端程序执行到下面来读消息时,就会把缓存的消息读出来,服务端程序一定能保证客户端发送的消息收到,因为是TCP可靠通信*/
/*服务端一旦接受了客户端的连接请求,那就得到一个socket对象,代表客户端一端,相当于是socket通信管道 */
//2.从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
OutputStream os = socket.getOutputStream();//获取后交给一个字节输出流的变量os记住
//接下来就可以用os写我们要发的数据//这里可以将原始的字节输出流(并不好用)进行一些包装
//包装成打印流或者高级的缓冲字节输出流,字符输出流,再用高级流写数据
//(数据输出流,数据输入流比较适合做通信)
//3.把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
//拿到数据管道之后剩下的都是IO流的操作了
//4.开始写数据出去
dos.writeUTF("在一起吧!");//直接写字符串出去
//消息发完之后就把数据输出流关闭掉(客户端一发)
dos.close();//关闭数据输出流时候,数据输出流是包含字节输出流的,直接关闭外部的包装流会自动把内部包装的字节输出流关闭
//接下来要把socket通信管道关闭
socket.close();//因为是要占系统资源,进行网路通信的
}
}
//服务端
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:完成TCP通信——服务端开发:实现一发一收
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----------服务端启动成功-----------");
//1.创建ServerSocket的对象,同时为服务端注册端口
ServerSocket serverSocket = new ServerSocket(8888);
//2.使用serverSocket对象,调用accept方法,等待客户端连接请求
Socket socket = serverSocket.accept();
/* 一旦调用程序就会暂停,阻塞等待客户端的连接!
* 客户端发起连接被收到了,连接通过了,服务端这边也会得到一个socket对象,
* 两个socket是一一对应的,端到端的通信管道 */
//3.服务端就可以从通信管道中收客户端发的消息,从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
//4.把原始的字节输入流包装成一个数据输入流
DataInputStream dis = new DataInputStream(is);
//5.使用数据输入流读取客户端发送的消息
String rs = dis.readUTF();//一一对应
System.out.println(rs);
//也可以获取客户端的IP地址
System.out.println(socket.getRemoteSocketAddress());
//一发一收,把数据输入流关闭
dis.close();
socket.close();
}
}
TCP通信-多发多收
案例:使用TCP通信实现:多发多收消息
1.客户端使用死循环,让用户不断输入消息。
2.服务端也使用死循环,控制服务端收完消息,继续等待接收下一个消息。
//客户端
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 目标:完成TCP通信——客户端开发:实现一发一收
*/
public class Client {
public static void main(String[] args) throws Exception {
//1.创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);
//2.从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
OutputStream os = socket.getOutputStream();
//3.把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
//拿到数据管道之后剩下的都是IO流的操作了
Scanner sc = new Scanner(System.in);
//4.开始写数据出去
while (true) {
System.out.println("请输入:");
String msg = sc.nextLine();//接收用户输入的消息
//一旦用户输入exit,就退出客户端程序
if ("exit".equals(msg)) {
System.out.println("退出成功!");
dos.close();
socket.close();
break;//跳出死循环用户就退出去了
}
dos.writeUTF(msg);//直接写字符串出去
dos.flush();//把数据刷新出去防止数据还在客户端的内存中,就是立即发送给服务端
}
}
}
//服务端
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 目标:完成TCP通信——服务端开发:实现一发一收
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----------服务端启动成功-----------");
//1.创建ServerSocket的对象,同时为服务端注册端口
ServerSocket serverSocket = new ServerSocket(8888);
//2.使用serverSocket对象,调用accept方法,等待客户端连接请求
Socket socket = serverSocket.accept();
//3.服务端就可以从通信管道中收客户端发的消息,从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
//4.把原始的字节输入流包装成一个数据输入流
DataInputStream dis = new DataInputStream(is);
//5.使用数据输入流读取客户端发送的消息
/*
如果客户端退出了,服务端就会报错,是因为端对端的通信,
服务端等客户端消息的时候就会出现异常说明客户端已经退出了
所以可以在服务端捕获这个异常,一旦捕获到异常就说明客户端离线了
*/
while (true) {
try {
String rs = dis.readUTF();
System.out.println(rs);
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了!");
dis.close();//资源也释放
socket.close();//跳出去之前也可以把socket管道进行关闭
break;//管道离线了就跳出循环
}
}
}
}
TCP通信-支持与多个客户端同时通信
一个服务端只能接收一个客户端,即使写了个死循环,但是循环第一轮只接第一个客户端连接,然后到内部这个死循环中不断的等第一个客户端的消息,没有办法执行第二轮,没有办法接第二个客户端的。之所以只能够接收一个客户端的通信连接,归根结底还是服务端只有一个线程,这一个线程只能用来不断的接收第一个客户端的消息,没有办法去接收第二个客户端的连接请求。
要支持与多个客户端同时通信就要用到多线程。 用一个死循环不断地接收客户端的连接请求,一旦与客户端连接,他就会得到一个socket通信管道,
while(true){
//2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
}
此时应该把这个socket通信管道交给一个独立的线程来进行处理,主线程只负责接收新的客户端连接,而不应该再让主线程再去读管道中的数据;一旦接到了新的客户端连接,我们应该把这个客户端对应的socket通信管道,交给一个独立的线程来处理,就是把数据读出来进行通信。
//服务端
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----------服务端启动成功-----------");
//1.创建ServerSocket的对象,同时为服务端注册端口
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
//2.使用serverSocket对象,调用accept方法,等待客户端连接请求
Socket socket = serverSocket.accept();
//3.把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
//要有线程,先定义线程类再创建线程
/*
有了线程类的架子,就可以在服务端这个地方每次得到socket通信管道之后,
应该把这个通信管道交给一个独立的线程来处理,就应该通过这个线程类new一个独立的线程,先启动
*/
new ServerReaderThread(socket).start();
//启动的目的是让这个线程去当前这个客户端的socket通信管道与他进行通信,
// 所以要把这个通信管道交给这个线程,才能去这个通信管道中读取数据(通过有参构造器交给线程-》定义一个有参构造器)
}
}
}
//多线程
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;
//继承线程类还要重新run方法
public class ServerReaderThread extends Thread{
private Socket socket;//成员变量类型就是socket类型
public ServerReaderThread(Socket socket) {
//接到当前这个客户端socket通信管道之后,应该在这个线程对象里面把这个socket管道给记住——在上面定义一个成员变量
this.socket = socket;//上面的socket=送进来的socket通信管道,这样就把当前客户端的通信管道交给了独立的线程对象来记住
//线程对象在启动的时候,就可以去socket管道中读当前送下来的客户端管道中的消息
}
//怎么读消息:代码在这写
@Override
public void run() {
try {
InputStream is = socket.getInputStream();//得到字节输入流再包装成数据输入流
DataInputStream dis = new DataInputStream(is);
//让线程不断去读当前这个客户端管道中的消息
while (true){
String msg = dis.readUTF();//得到读到的消息
System.out.println(msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//客户端
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);
//2.从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
OutputStream os = socket.getOutputStream();
//3.把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
//拿到数据管道之后剩下的都是IO流的操作了
Scanner sc = new Scanner(System.in);
//4.开始写数据出去
while (true) {
System.out.println("请输入:");
String msg = sc.nextLine();//接收用户输入的消息
//一旦用户输入exit,就退出客户端程序
if ("exit".equals(msg)) {
System.out.println("退出成功!");
dos.close();
socket.close();
break;//跳出死循环用户就退出去了
}
dos.writeUTF(msg);//直接写字符串出去
dos.flush();//把数据刷新出去防止数据还在客户端的内存中,就是立即发送给服务端
}
}
}
- 怎么样去追踪客户端的上线和下线逻辑呢?
每个客户端上线都要经过这个地方,
都是由服务端的主线程负责把客户端的管道接进来,一旦接了一个客户端管道就代表有一个客户端上线了,可以通过socket通信管道拿到客户端他的IP地址socket.getRemoteSocketAddrress(),拿到IP地址和端口;
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress()+"上线了!");
下线:会在这里不断的读数据,如果线程在守这个管道的时候——线程在读数据的时候客户端管道突然下线了,代码就会出异常,try/catch拦截异常,代表当前读的管道对应的客户端离线了,才会出异常被拦截。
try {//客户端离线了才会出异常被拦截
String msg = dis.readUTF();//得到读到的消息
System.out.println(msg);
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "下线了!");
//下线了,资源释放
dis.close();
socket.close();
break;//跳出循环,不再受客户端管道的消息,线程也就挂掉了
}
TCP通信-综合案例
群聊:客户端--->多个客户端
客户端和客户端实际上是不能够直接通信的,还是先要准备一个服务端, 让客户端先连接到服务端,再想办法实现群聊,服务端还是要有一个主线程,使用死循环不断地接收客户端的通信管道连接。怎么实现群聊?子线程1会把当前这个客户端发过来的消息分发给其他所以的在线socket管道,又流到他们的客户端,实现群聊,端口转发思想。
服务端如何知道有多少客户端管道是在线的?服务端需要提供一个集合,存储所有的在线socket管道,方便转发消息。
只定义一个集合(在服务端),可以用静态修饰
public static List<Socket> onLineSockets = new ArrayList<>();
什么时候把在线的socket管道存进去呢?只要有一个客户端连过来就应该把通信管道存到在线集合里面去
在调用accept方法,等待客户端连接请求之后(在服务端)
onLineSockets.add(socket);
怎么样把离线的通信管道从中间抹掉呢?管道是交给线程处理的(在服务端线程类)
Server.onLineSockets.remove(socket);//可以用Server访问到在线的集合然后把离线的socket从中间抹掉
群聊的逻辑:某一个客户端的消息发送给我们服务端,服务端用一个线程负责从客户端管道里面读他的消息,每读到一个客户端消息(进入到线程里面来),把这个 消息分发给全部客户端进行接收。建议设计成方法,独立功能独立成方法。sendMsgToAll(msg) (在服务端线程类)
private void sendMsgToAll(String msg) throws Exception {
//收到一条消息,发送给所有的在线socket管道(Server类的静态集合里面)接收
//遍历集合,用Server这个类.onLineSockets集合
for (Socket onLineSocket : Server.onLineSockets) {
//推消息是得到一个输出流
OutputStream os = onLineSocket.getOutputStream();
//再把输出流包装成数据输出流->来发消息比较方便
DataOutputStream dos = new DataOutputStream(os);
//通过数据输出流为当前在线socket推送一个消息
dos.writeUTF(msg);
dos.flush();//刷新
//此时服务端满足将每次发来的消息接住,只需要客户端实现将每次的消息接收
//--》单独定义一个线程出来为客户端管道不断的收服务端转发过来的消息
}
}
此时服务端满足将每次发来的消息接住,只需要客户端实现将每次的消息接收,单独定义一个线程出来为客户端管道不断的收服务端转发过来的消息。(在客户端线程类)
//新建一个线程类——独立线程
import java.io.DataInputStream;
import java.io.IOException;
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 (IOException e) {
System.out.println("自己下线了。" + socket.getRemoteSocketAddress());
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
现在要客户端不断的收消息,创建一个独立的线程,负责随机从socket管道中接收服务端发送过来的消息(在客户端)
new ClientReaderThread(socket).start();//new新建好的线程再把socket扔进去再启动
这是一个典型的CS架构,客户端服务端都需要我们自己来写。
以下是一个BS架构,BS架构是不需要我们去开发客户端,直接开发服务端就可以了,BS架构是使用浏览器作为客户端的,并且是在浏览器里面去访问服务端程序并立即让服务端程序响应一个网页给他进行展示。(为将来学习网站开发打好基础)
//服务端
import com.itheima.d6_tcp3.ServerReaderThread;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("----------服务端启动成功----------");
//1.创建ServerSocket的对象,同时为服务端注册端口
ServerSocket serverSocket = new ServerSocket(8080);
while(true){
//2.使用serverSocket对象,调用一个accept方法,等待客户端的请求连接
Socket socket = serverSocket.accept();
//3.把这个客户端对应的socket通信管道,交给一个独立的线程负责处理
new ServerReaderThread(socket).start();
}
}
}
到底好不好?每次请求都开一个新线程?——高并发时,容易宕机! 使用线程池进行优化
//服务端
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 Server {
public static void main(String[] args) throws Exception {
System.out.println("----------服务端启动成功----------");
//1.创建ServerSocket的对象,同时为服务端注册端口
ServerSocket serverSocket = new ServerSocket(8080);
//每个线程是往客户端管道响应一个网页回去属于IO密集型,发数据占时间比较多
//创建出一个线程池,负责处理通信管道的任务
ThreadPoolExecutor pool = new ThreadPoolExecutor(8 * 2, 8 * 2, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
while (true) {
//2.使用serverSocket对象,调用一个accept方法,等待客户端的请求连接
Socket socket = serverSocket.accept();
/*
一旦这个地方接到一个客户端的通信管道之后就应该把通信管道封装成一个任务对象,交给线程池来处理
如何封装成任务管道?只需要把线程类改成任务类就可以——线程类其实就是任务类
*/
//3.把这个客户端对应的socket通信管道,交给一个独立的线程负责处理
//new ServerReaderThread(socket).start();
//任务类是干嘛的?可以用这个任务类来创建对象
pool.execute(new ServerReaderRunnable(socket));//把当前客户端的任务管道交给任务对象(把通信管道封装成一个任务对象),任务对象再交给线程池
//线程池就会用它固定的线程数量处理任务对象——执行的就是run方法,就是从当前的通信管道里面为他响应一个网页
//把通信管道封装成一个任务对象,任务对象一执行就是为任务管道响应一个网页
}
}
}
//任务类
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
public class ServerReaderRunnable implements Runnable {
private Socket socket;
public ServerReaderRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//立即相应一个内容给浏览器
try {
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=UTF-8");
ps.println();//必须换行
ps.println("<div style='color:red;font-size:120px;text-align:center'>我爱你</div>");
ps.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}