Java网络通信
前言
1. 基本知识
通俗一点就是CS就是要下载应用来访问的,BS是在浏览器上面访问的,不用下载
1.1 IP
IP地址就是你电脑的主机号,一台设备都有唯一的IP,端口就是程序的唯一标识
这上面就是我们的ip地址了,有IPv4和IPv6两种
因为IPv4不够用了,所以才用IPv6的
IP域名和IP就是一个东西
只要有网还有IP地址存在,那么ping就会成功,
127.0.0.1就是我们这个电脑的IP地址,默认是
每个电脑的127.0.0.1都是自己的IP
//1.获取本机IP地址对象
InetAddress ip1=InetAddress.getLocalHost();
System.out.println(ip1.getHostName());//获取本机IP地址对象的名字
System.out.println(ip1.getHostAddress());//获取本机IP地址对象的IP
我这个就叫hahaha
//2.获取指定IP或者域名的IP地址对象
InetAddress ip2=InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());
然后我直接在网址输入这个IP
就真的变成百度了
System.out.println(ip2.isReachable(6000));
这个就是看6秒内,我们本机能否和百度IP建立联系,当然有网就可以了
1.2 端口与通信协议
因为视频语音的话,少了几个包最多只是声音变小,卡了一下而已
三次握手的意思就是,第一次确认了客户端可以发消息,第二次确认了服务器端可以收消息,和发消息,第三次就是确认客户端可以接受消息,这样两边都可以收发消息了,就没有问题了
2. UDP通信
2.1 一发一收
public class Server {
public static void main(String[] args) throws Exception {
//创建一个服务器对象(创建一个接菜的人),注册端口
DatagramSocket socket=new DatagramSocket(6666);//这个对象也就是这个进程的端口是6666,可以指定端口的,不然就是随机的
//2.创建一个数据包对象,用于接收数据,就是创建一个盘子
byte[] buffer=new byte[1024*64];//因为一个数据包最大发送的内容就是这么大
DatagramPacket packet=new DatagramPacket(buffer,buffer.length);
//3.开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);//只管接收就可以了,只要把IP和端口告诉客户端,只管输入就可以了,这边只管接收
//接收到的内容在buffer中,然后就是接收到了多少就倒出多少,不可能倒64kb吧
int len=packet.getLength();
String rs=new String(buffer,0,len);
System.out.println(rs);
}
}
这个是服务端
public class Client {
public static void main(String[] args) throws Exception {
//1.创建客户端对象(发菜的人)
DatagramSocket socket=new DatagramSocket();
//包装数据包对象分装要发出去的数据(创建盘子)
byte[] bytes="我是快乐的客户端,我爱你abc".getBytes();
// public DatagramPacket(byte buf[], int length,
// InetAddress address, int port) {
// this(buf, 0, length, address, port);
// }
//参数一:分装要发出去的数据,参数二:发送出去的数据大小(字节个数),参数三:服务端的IP地址,参数四:服务端程序的端口
DatagramPacket packet=new DatagramPacket(bytes,bytes.length, InetAddress.getLocalHost(),6666);//包装菜和盘子
socket.send(packet);//发出去
System.out.println("发送完毕");
socket.close();
}
}
这个是客户端
应该先启动服务端,因为如果先启动客户端的话,直接就发送了,也不会管你发送成功了没有,还没等服务端接受就没了
只要服务端启动了,那么就会停在那个receive那里,等待接受
然后就是服务端接受到了的话,也可以得到客户端的IP和端口,还有就是资源不要忘了关闭,关闭客户端,服务端就可以了
System.out.println(packet.getAddress().getHostAddress());
System.out.println(packet.getPort());
socket.close();
然后就是其实客户端可以创建一个指定端口的对象
//1.创建客户端对象(发菜的人)
DatagramSocket socket=new DatagramSocket(7777);
2.2 多发多收
下面我们来实现多个客户端发送的场景
用个while循环就可以了
public class Client {
public static void main(String[] args) throws Exception {
DatagramSocket socket=new DatagramSocket(7777);
Scanner sc=new Scanner(System.in);
while (true) {
System.out.println("请说");
String msg=sc.nextLine();
if("exit".equals(msg))
{
System.out.println("退出成功");
socket.close();
break;
}
byte[] bytes=msg.getBytes();
DatagramPacket packet=new DatagramPacket(bytes,bytes.length, InetAddress.getLocalHost(),6666);//包装菜和盘子
socket.send(packet);//发出去
}
}
}
这样就可以了,那么服务端也要循环来接收
public class Server {
public static void main(String[] args) throws Exception {
//创建一个服务器对象(创建一个接菜的人),注册端口
DatagramSocket socket=new DatagramSocket(6666);
byte[] buffer=new byte[1024*64];
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);
System.out.println("--------------------");
}
}
}
因为服务端一般是不会关闭的,所以不用close
现在我们就可以客户端一直输入,服务端一直接受了
但是如何启动多个客户端呢,我们的编译器默认的是只能启动一个,但是可以改
点编辑配置
点允许多个实例
但是还是会报错,为什么呢,因为我们的客户端的端口指定了为7777
,不可能两个程序用一个端口吧,所以我们还是让系统随机分配端口吧
DatagramSocket socket=new DatagramSocket();
这样就可以了
3. TCP通信
3.1 一发一收
public class Client {
public static void main(String[] args) throws Exception {
//创建socket对象,并请求与服务端连接
Socket socket=new Socket("127.0.0.1",8888);//连接的就是IP为127.0.0.1,端口为8888的服务端
//2.从socket通信管道中得到一个字节输出流,用来发数据给服务端程序
OutputStream os=socket.getOutputStream();
//3.把这个低级的字节输出流包装成数据输出流
DataOutputStream dos=new DataOutputStream(os);
//4.写数据出去
dos.writeUTF("在一起毫秒");
dos.close();
socket.close();//要释放两个资源,一个网络的,一个IO流的
}
}
public class Server {
public static void main(String[] args) throws Exception {
//创建一个ServerSocket的对象,同时为服务端注册端口
ServerSocket serverSocket=new ServerSocket(8888);
//使用serverSocket对象调用一个accept方法,等待客户端的连接请求,返回一个socket,与客户端对应,在服务端但是
Socket socket=serverSocket.accept();
//从socket通信管道中得到一个字节输入流
InputStream is=socket.getInputStream();
//包装成高级的IO流
DataInputStream dis=new DataInputStream(is);
//使用数据输入流读取客户端发送过来的消息
String rs=dis.readUTF();
System.out.println(rs);
//打印客户端的IP
System.out.println(socket.getRemoteSocketAddress());
dis.close();
socket.close();
}
}
这个服务端会先在accept那里等着连接客户端,成功后,就会在readUTF等待读
3.2 多发多收
public class Client {
public static void main(String[] args) throws Exception {
Socket socket=new Socket("127.0.0.1",8888);
OutputStream os=socket.getOutputStream();
DataOutputStream dos=new DataOutputStream(os);
Scanner sc=new Scanner(System.in);
while(true){
System.out.println("请说:");
String msg=sc.nextLine();
if("exit".equals(msg)){
System.out.println("退出");
dos.close();
socket.close();
break;
}
dos.writeUTF(msg);
dos.flush();//刷新过去,防止在缓冲区中
}
}
}
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket=new ServerSocket(8888);
Socket socket=serverSocket.accept();
InputStream is=socket.getInputStream();
DataInputStream dis=new DataInputStream(is);
while(true){
String rs=dis.readUTF();
System.out.println(rs);
}
}
}
但是在我们客户端推出的时候,服务端就会异常了,因为一端断开了,read不了了,但是还在等,就会出错,所以要捕获
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket=new ServerSocket(8888);
Socket socket=serverSocket.accept();
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()+"离线了");
dis.close();
socket.close();
break;
}
}
}
}
但是我们这个不能实现服务端与多个用户通信,因为socket始终只有一个,而且又不确定用户端有多少个
所以我们就要用多线程的处理思想了
3.3 与多个用户通信
我们可以这样,用主线程来接收客户端的连接,连接了一个的话,就把这个socket交给一个子线程来处理
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
}
}
先自定义一个线程,因为要把socket交给线程,所以线程的定义要有socket
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket=new ServerSocket(8888);
while(true){
Socket socket=serverSocket.accept();
new ServerReaderThread(socket).start();//交给一个新的线程来处理
}
}
}
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 (IOException e) {
System.out.println(socket.getRemoteSocketAddress()+"离线了");//结束这个线程
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这样的话,主线程就会在accept那里等着,子线程的话,就会在read那里等着
这样我们就可以开启多个客户端了
3.4 用户与用户之间通信
其实实现逻辑就是客户端发给服务端,服务端发给其他客户端
就是一个客户端发送消息,其他客户端都能看到
socket用集合存着,这个集合只有一个
public class Server {
public static List<Socket> onLineSockets=new ArrayList<>();
public static void main(String[] args) throws Exception {
ServerSocket serverSocket=new ServerSocket(8888);
while(true){
Socket socket=serverSocket.accept();
onLineSockets.add(socket);
System.out.println("有人上线了"+socket.getRemoteSocketAddress());
new ServerReaderThread(socket).start();
}
}
}
接收到一个,就增加一个
对应还有删除,
System.out.println(socket.getRemoteSocketAddress()+"离线了");//结束这个线程
Server.onLineSockets.remove(socket);//
还要把这个socket发给其他用户
分发给全部客户端就可以了
String rs = dis.readUTF();
System.out.println(rs);
sendRsToAll(rs);
private void sendRsToAll(String rs) throws Exception {
for (Socket onLineSocket : Server.onLineSockets) {
OutputStream os=onLineSocket.getOutputStream();
DataOutputStream dos=new DataOutputStream(os);
dos.writeUTF(rs);
dos.flush();
}
}
然后对应每个客户端都要接受来自对应服务端的数据
因为socket是对应的,所以一定会对应回去的
每个客户端都要一直读,所以这也是一个死循环,而且还要一直死循环写,所以又是多线程的逻辑
又要建立一个新的线程
这个的逻辑和服务端的读的线程逻辑是差不多的
public class ClientReaderThread extends Thread{
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 rs = dis.readUTF();
System.out.println(socket.getRemoteSocketAddress()+"说"+rs);
} catch (IOException e) {
Server.onLineSockets.remove(socket);//
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Client {
public static void main(String[] args) throws Exception {
Socket socket=new Socket("127.0.0.1",8888);
new ClientReaderThread(socket).start();//创建一个线程来专门接受读的
OutputStream os=socket.getOutputStream();
DataOutputStream dos=new DataOutputStream(os);
Scanner sc=new Scanner(System.in);
while(true){
System.out.println("请说:");
String msg=sc.nextLine();
if("exit".equals(msg)){
System.out.println("退出");
dos.close();
socket.close();
break;
}
dos.writeUTF(msg);
dos.flush();//刷新过去,防止在缓冲区中
}
}
}
public class Server {
public static List<Socket> onLineSockets=new ArrayList<>();
public static void main(String[] args) throws Exception {
ServerSocket serverSocket=new ServerSocket(8888);
while(true){
Socket socket=serverSocket.accept();
onLineSockets.add(socket);
System.out.println("有人上线了"+socket.getRemoteSocketAddress());
new ServerReaderThread(socket).start();
}
}
}
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);
sendRsToAll(rs);
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress()+"离线了");//结束这个线程
Server.onLineSockets.remove(socket);//
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void sendRsToAll(String rs) throws Exception {
for (Socket onLineSocket : Server.onLineSockets) {
OutputStream os=onLineSocket.getOutputStream();
DataOutputStream dos=new DataOutputStream(os);
dos.writeUTF(rs);
dos.flush();
}
}
}
3.5 实现一个简易版的BS架构
我们的服务端只有一个,不同浏览器打开我们的服务端的时候,就会创建不同的子线程
public class Server {
public static List<Socket> onLineSockets=new ArrayList<>();
public static void main(String[] args) throws Exception {
ServerSocket serverSocket=new ServerSocket(8888);
while(true){
Socket socket=serverSocket.accept();
onLineSockets.add(socket);
System.out.println("有人上线了"+socket.getRemoteSocketAddress());
new ServerReaderThread(socket).start();
}
}
}
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
//连接到了,就要返回一个‘黑马程序员给浏览器’
try {
OutputStream os=socket.getOutputStream();
DataOutputStream dos=new DataOutputStream(os);
dos.writeUTF("黑马程序员");
dos.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后我们就可以在浏览器上连接我们的服务端了
但是这样不行呢,为什么呢,因为服务器必须给浏览器响应http协议规定的数据格式,不然浏览器不识别返回的数据。
而且浏览器会疯狂去试探,所以有很多上线了,但是没有一个能认识那些数据,所以会出错
因为换行数据流不好操作,所以我们用打印流
我们返回的数据就要是上面的格式,不然浏览器无法识别,这个就是http协议
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(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("黑马程序员");
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
可以美化一下
ps.println("<div style='color:red;font-size:120px;text-align:center'>黑马程序员");
换个浏览器也可以
拓展一下,我们可以使用线程池
//创建一个线程池,,因为这个是IO型任务,所以核心线程数量=CPU核数*2
ThreadPoolExecutor pool=new ThreadPoolExecutor(16*2,16*2,0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
因为为线程池,所以传入的也都是任务对象,就不能是线程对象了
public class ServerReaderRunnable implements Runnable{
public class Server {
public static List<Socket> onLineSockets=new ArrayList<>();
public static void main(String[] args) throws Exception {
ServerSocket serverSocket=new ServerSocket(8888);
//创建一个线程池,,因为这个是IO型任务,所以核心线程数量=CPU核数*2
ThreadPoolExecutor pool=new ThreadPoolExecutor(16*2,16*2,0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
while(true){
Socket socket=serverSocket.accept();
pool.execute(new ServerReaderRunnable(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'>黑马程序员");
ps.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这样就可以了
总结
后面还是接着讲Java