Socket学习记录

本次学习Socket的编程开发,该技术在一些通讯软件,比如说微信,QQ等有广泛应用。

网络结构

这些都是计算机网络中的内容,我们在这里简单回顾一下:

UDP(User Datagram Protocol):用户数据报协议;TCP(Transmission ControlProtocol):传输控制协议。

TCP协议

特点:面向连接、可靠通信。
TCP的最终目的:要保证在不可靠的信道上实现可靠的传输。
TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。

四次握手是为了确保收发数据都已完成。

我们首先了解一下关于获取主机地址的相关方法:

java 复制代码
		//获取本机IP地址对象的地址
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println(localHost.getHostName());
        System.out.println(localHost.getHostAddress());
        //获取指定IP域名的IP地址对象
        InetAddress ip = InetAddress.getByName("www.baidu.com");
        System.out.println(ip.getHostName());//输出ip主机名称
        System.out.println(ip.getHostAddress());//输出指定域名的ip地址
        //判断6秒内能否与百度联通,相当于ping
        System.out.println(ip.isReachable(6000));

UDP通信开发

随后我们进行客户端与服务端的数据发送与接收:

首先是客户端的定义:

java 复制代码
		//创建客户端对象,这里可以选择使用无参构造,当然也可以指定端口进行有参构造,在不指定端口时系统会默认分配
        DatagramSocket socket = new DatagramSocket();
        //创建数据包封装对象,存储数据信息
//        public DatagramPacket(byte buf[], int offset, int length,
//        InetAddress address, int port) {
//            setData(buf, offset, length);
//            setAddress(address);
//            setPort(port);
//        }
        byte[] bytes = "客户端消息:我是鹏翔".getBytes();
        //客户端发送的数据包,需要指明接收的服务端的IP地址以及端口
        DatagramPacket packet =
                new DatagramPacket(bytes,bytes.length,InetAddress.getLocalHost(),8888);
        //发送数据包
        socket.send(packet);
        System.out.println("客户端数据发送完毕");
        socket.close();

服务端的开发设计

java 复制代码
		System.out.println("服务端启动");
        //创建一个服务端对象,并指定端口
        DatagramSocket socket = new DatagramSocket(8888);
        //定义所能够接收的数据的大小
        byte[] buffer = new byte[1024*64];
        //服务器接受的数据包
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length);

        //接收数据
        socket.receive(packet);
        int length = packet.getLength();
        String string = new String(packet.getData(), 0, length);//发送多少数据则接收多少数据
        System.out.println(string);
        socket.close();

至此,我们的客户端与服务端便开发完成了,在实验中,我们需要先启动服务端,再启动客户端。

至此,完成客户端与服务端的消息发送与接收。

UTP通信多发多收

但这只是完成了一次消息的发送与接收,而在实际情况中我们往往需要进行多发多收,那么该如何实现呢?

客户端设计:

java 复制代码
public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        //创建一个服务端对象,并指定端口
        DatagramSocket socket = new DatagramSocket(8888);
        //定义所能够接收的数据的大小
        byte[] buffer = new byte[1024*64];
        //服务器接受的数据包
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
        while(true){
            socket.receive(packet);
            int length = packet.getLength();
            String string = new String(packet.getData(), 0, length);
            System.out.println(string);
            System.out.println("--------------------------");
        }
    }

客户端设计:

java 复制代码
public static void main(String[] args) throws IOException {
        //创建客户端对象
        DatagramSocket socket = new DatagramSocket();
        Scanner scanner=new Scanner(System.in);
        while(true){
            System.out.println("请说:");
            String msg = scanner.nextLine();
            if ("exit".equals(msg)) {
                System.out.println("客户端数据发送完毕");
                socket.close();
                break;
            }
            byte[] bytes = msg.getBytes();
            //客户端发送的数据包,需要指明接收的服务端的IP地址以及端口
            DatagramPacket packet =
                    new DatagramPacket(bytes,bytes.length,InetAddress.getLocalHost(),8888);
            //发送数据包
            socket.send(packet);
        }

此外,服务器是否能够接收多个客户端发送的消息呢,当然可以,只需要将客户端程序设置为允许多开即可。

TCP通信开发

public ServerSocket(int port) 为服务端程序注册端口

public Socket accept()方法:阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象。

客户端设计实现

java 复制代码
		Socket socket = new Socket("127.0.0.1",8088);
        //从socket中获取一个字节输出流,用于给服务端发送
        OutputStream outputStream = socket.getOutputStream();
        //原本的字节输出流并不好用,将其封装为高级的数据输出流
        DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
        //开始写数据
        dataOutputStream.writeUTF("我爱你!");
        dataOutputStream.close();
        socket.close();

服务端设计实现

java 复制代码
		System.out.println("服务端启动");
        //创建ServerSocket对象,并指明端口号,方便接收客户端数据
        ServerSocket serverSocket = new ServerSocket(8088);
        //调用accept方法,等待客户端的连接请求
        Socket accept = serverSocket.accept();
        //从socket的通信管道中得到一个字节输入流
        InputStream inputStream = accept.getInputStream();
        //将原始的字节输入流包装成高级的数据输入流
        DataInputStream dataInputStream = new DataInputStream(inputStream);
        //使用数据输入流读取客户端发送的数据
        String string = dataInputStream.readUTF();//通信很严格,要保持一致
        System.out.println(string);
        System.out.println(accept.getInetAddress());//输出发送客户端的IP地址
        dataInputStream.close();
        serverSocket.close();

同样的,我们只是完成一条消息的发送与接收,那么该如何实现数据的多发多收呢?

TCP通信多发多收

其实实现与UDP时的完全相同,只需要一个循环即可。

全选要加入循环的语句,按住Ctrl+Alt+T

多发多收服务端设计:

java 复制代码
public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        //创建ServerSocket对象,并指明端口号,方便接收客户端数据
        ServerSocket serverSocket = new ServerSocket(8088);
        //调用accept方法,等待客户端的连接请求
        Socket accept = serverSocket.accept();
        //从socket的通信管道中得到一个字节输入流
        InputStream inputStream = accept.getInputStream();
        //将原始的字节输入流包装成高级的数据输入流
        DataInputStream dataInputStream = new DataInputStream(inputStream);
        //使用数据输入流读取客户端发送的数据
        while (true) {
            String string = dataInputStream.readUTF();//通信很严格,要保持一致
            System.out.println(string);
            System.out.println(accept.getInetAddress());//输出发送客户端的IP地址
        }
    }

多发多收客户端设计:

java 复制代码
 public static void main(String[] args) throws IOException {
        //创建Socket对象,并同时请求服务端程序的连接,声明服务器的IP与端口号
        Socket socket = new Socket("127.0.0.1",8088);
        //从socket中获取一个字节输出流,用于给服务端发送
        OutputStream outputStream = socket.getOutputStream();
        //原本的字节输出流并不好用,将其封装为高级的数据输出流
        DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
        //开始写数据
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("请输入内容:");
            String string = scanner.nextLine();
            if ("exit".equals(string)) {
                dataOutputStream.close();
                socket.close();
                System.out.println("退出成功!");
                break;
            }
            dataOutputStream.writeUTF(string);
            dataOutputStream.flush();//将数据刷新出去,防止数据还留在内存中
        }
    }

同时需要注意,TCP作为可靠连接,一旦服务端挂掉了,那么就会抛出异常

我们可以通过捕获抛出的异常,来判断是否客户端退出。

java 复制代码
public class Servers {
    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        //创建ServerSocket对象,并指明端口号,方便接收客户端数据
        ServerSocket serverSocket = new ServerSocket(8088);
        //调用accept方法,等待客户端的连接请求
        Socket accept = serverSocket.accept();
        //从socket的通信管道中得到一个字节输入流
        InputStream inputStream = accept.getInputStream();
        //将原始的字节输入流包装成高级的数据输入流
        DataInputStream dataInputStream = new DataInputStream(inputStream);
        //使用数据输入流读取客户端发送的数据
        while (true) {
            try {
                String string = dataInputStream.readUTF();//通信很严格,要保持一致
                System.out.println(string);
            } catch (IOException e) {
                System.out.println(accept.getInetAddress()+"客户端退出了!");//输出发送客户端的IP地址
                dataInputStream.close();
                serverSocket.close();
                break;
            }
        }

    }
}

TCP通信聊天室

如何实现一个服务器与多个客户端通信呢?现在的肯定是不行的,因为我们在判断客户端关闭后也将服务端关闭了,事实上,此时服务端只能和一个客户端建立可靠连接,归根接地,是因为在建立连接后,服务端一直在等待某一个客户端发送的消息,这就导致会停留在那,从而无法与其他客户端建立连接。怎么办呢?可以使用多线程来解决。

改进后的服务端:

java 复制代码
package IPAddress.TCP;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerMany {
    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        //创建ServerSocket对象,并指明端口号,方便接收客户端数据
        ServerSocket serverSocket = new ServerSocket(8088);
        //调用accept方法,等待客户端的连接请求
        Socket accept = null;
        while (true) {
            accept = serverSocket.accept();
            new ServerReadThread(accept).start();
        }
    }
}

多开线程实现服务端接收数据

java 复制代码
package IPAddress.TCP;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class ServerReadThread extends Thread{
    private Socket socket;
    public ServerReadThread(Socket accept) {
        this.socket=accept;
    }

    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            //将原始的字节输入流包装成高级的数据输入流
            DataInputStream dataInputStream = new DataInputStream(inputStream);
            System.out.println(socket.getInetAddress()+"客户端上线了!");//输出发送客户端的IP地址
            //使用数据输入流读取客户端发送的数据
            while (true) {
                try {
                    String string = dataInputStream.readUTF();//通信很严格,要保持一致
                    System.out.println(string);
                } catch (IOException e) {
                	System.out.println(socket.getInetAddress()+"客户端下线了!");
                    dataInputStream.close();
                    socket.close();
                    break;
                }
            }
        } catch (IOException e) {
            System.out.println("服务异常!" + socket.getInetAddress() + "连接中断!");
        }
    }
}

最后,我们可以通过一个聊天室的案例来简单检验一下成果:

首先我们需要在服务端定义一个集合,用于保存连接的socket,同时由主线程负责创建socket连接,一旦有新的客户端开启,则开启一个新的子线程,用于该客户端与服务端之间的通信:

java 复制代码
public class Server {
    public static List<Socket> sockets=new ArrayList<>();
    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        //创建ServerSocket对象,并指明端口号,方便接收客户端数据
        ServerSocket serverSocket = new ServerSocket(8088);
        //调用accept方法,等待客户端的连接请求
        Socket accept = null;
        while (true) {
            accept = serverSocket.accept();
            sockets.add(accept);
            new ServerReadThread(accept).start();
        }
    }
}

在服务端的子线程中,负责将接收的信息输出,并将接收的信息转发给其他客户端(端口转发)

java 复制代码
import java.io.*;
import java.net.Socket;

public class ServerReadThread extends Thread{
    private Socket socket;
    public ServerReadThread(Socket accept) {
        this.socket=accept;
    }

    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            //将原始的字节输入流包装成高级的数据输入流
            DataInputStream dataInputStream = new DataInputStream(inputStream);
            System.out.println(socket.getInetAddress()+"客户端上线了哟!");//输出发送客户端的IP地址
            //使用数据输入流读取客户端发送的数据
            while (true) {
                try {
                    String string = dataInputStream.readUTF();//通信很严格,要保持一致
                    System.out.println(string);
                    sendMsg(string);
                } catch (IOException e) {
                    System.out.println(socket.getInetAddress()+"客户端下线了!");
                    Server.sockets.remove(socket);
                    dataInputStream.close();
                    socket.close();
                    break;
                }
            }
        } catch (IOException e) {
            System.out.println("服务异常!" + socket.getInetAddress() + "连接中断!");
        }
    }

    private void sendMsg(String string) throws IOException {
        //发送给所有Socket管道去接收
        System.out.println("转发数据");
        for (Socket online:Server.sockets) {
            OutputStream outputStream = online.getOutputStream();
            DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
            dataOutputStream.writeUTF(string);
            dataOutputStream.flush();
        }
    }
}

在客户端设计中,除了原本的输入数据发送信息外,还要开启一个线程用于接收服务器转发的数据:

java 复制代码
public class Clients {
    public static void main(String[] args) throws IOException {
        //创建Socket对象,并同时请求服务端程序的连接,声明服务器的IP与端口号
        Socket socket = new Socket("127.0.0.1",8088);
        new ClientReadThread(socket).start();
        //从socket中获取一个字节输出流,用于给服务端发送
        OutputStream outputStream = socket.getOutputStream();
        //原本的字节输出流并不好用,将其封装为高级的数据输出流
        DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
        //开始写数据
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("请输入内容:");
            String string = scanner.nextLine();
            if ("exit".equals(string)) {
                dataOutputStream.close();
                socket.close();
                System.out.println("退出成功!");
                break;
            }
            dataOutputStream.writeUTF(string);
            dataOutputStream.flush();//将数据刷新出去,防止数据还留在内存中
        }
    }
}

客户端接收转发信息的线程设计如下:

java 复制代码
public class ClientReadThread extends Thread{
    private Socket socket;
    public ClientReadThread(Socket accept) {
        this.socket=accept;
    }
    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            //将原始的字节输入流包装成高级的数据输入流
            DataInputStream dataInputStream = new DataInputStream(inputStream);
            //使用数据输入流读取客户端发送的数据
            while (true) {
                try {
                    String string = dataInputStream.readUTF();//通信很严格,要保持一致
                    System.out.println(string);
                } catch (IOException e) {
                    System.out.println("自己客户端下线了!");
                    dataInputStream.close();
                    socket.close();
                    break;
                }
            }
        } catch (IOException e) {
            System.out.println("服务异常!" + socket.getInetAddress() + "连接中断!");
        }
    }
}

BS架构通信开发

首先了解一下BS架构的基本原理:

BS架构下,我们并不需要开发客户端程序。只需要开发服务端即可

java 复制代码
public class Server {
    public static List<Socket> sockets=new ArrayList<>();
    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        //创建ServerSocket对象,并指明端口号,方便接收客户端数据
        ServerSocket serverSocket = new ServerSocket(9090);
        while (true) {
            Socket accept = serverSocket.accept();
            System.out.println("子线程启动");
            sockets.add(accept);
            new ServerReadThread(accept).start();
        }
    }
}

随后进行服务端子进程的设计,用于向浏览器响应一段文字,注意,要想向浏览器响应内容,就需要遵循固定的HTTP协议规定,即符合下面的要求:

服务端子线程设计如下:

java 复制代码
public class ServerReadThread extends Thread{
    private Socket socket;
    public ServerReadThread(Socket accept) {
        this.socket=accept;
    }

    @Override
    public void run() {
        try {
            System.out.println("上线访问了");
            OutputStream outputStream = socket.getOutputStream();
            //将原始的字节输出流包装成高级的打印流
            PrintStream printStream = new PrintStream(outputStream);
            printStream.println("HTTP/1.1 200 OK");
            printStream.println("Content-Type:text/html;charset=UTF-8");
            printStream.println();
            printStream.println("<div style='color:red;font-size:40px;text-align:center'>服务端响应</div>");
            printStream.close();
            socket.close();
        } catch (IOException e) {
            System.out.println("服务异常!" + socket.getInetAddress() + "连接中断!");
        }
    }
}

前面的BS架构设计中,每当浏览器发起一次访问,就会创建一个线程,然而,当我们的网站访问量十分大时,即面对一些高并发情况,就会出现宕机现象。对于这种情况,可以通过线程池来进行优化。

在主线程中设计一个线程池来控制线程数量

java 复制代码
public class Server {
    public static List<Socket> sockets=new ArrayList<>();
    public static void main(String[] args) throws IOException {
        System.out.println("服务端启动");
        //创建ServerSocket对象,并指明端口号,方便接收客户端数据
        ServerSocket serverSocket = new ServerSocket(9090);
        //通过线程池来控制执行线程的数量与任务队列数量
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                16 * 2, 16 * 2,
                0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(8),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        while (true) {
            Socket accept = serverSocket.accept();
            System.out.println("子线程启动");
            threadPoolExecutor.execute(new ServerReadRunnable(accept));
        }
    }
}

将原本的线程改造为任务。

java 复制代码
public class ServerReadRunnable implements Runnable{
    private Socket socket;
    public ServerReadRunnable(Socket accept) {
        this.socket=accept;
    }

    @Override
    public void run() {
        try {
            System.out.println("上线访问了");
            OutputStream outputStream = socket.getOutputStream();
            //将原始的字节输出流包装成高级的打印流
            PrintStream printStream = new PrintStream(outputStream);
            printStream.println("HTTP/1.1 200 OK");
            printStream.println("Content-Type:text/html;charset=UTF-8");
            printStream.println();
            printStream.println("<div style='color:red;font-size:40px;text-align:center'>服务端响应</div>");
            printStream.close();
            socket.close();
        } catch (IOException e) {
            System.out.println("服务异常!" + socket.getInetAddress() + "连接中断!");
        }
    }
}
相关推荐
wclass-zhengge5 分钟前
数据结构篇(绪论)
java·数据结构·算法
何事驚慌5 分钟前
2024/10/5 数据结构打卡
java·数据结构·算法
结衣结衣.6 分钟前
C++ 类和对象的初步介绍
java·开发语言·数据结构·c++·笔记·学习·算法
TJKFYY8 分钟前
Java.数据结构.HashSet
java·开发语言·数据结构
kylinxjd9 分钟前
spring boot发送邮件
java·spring boot·后端·发送email邮件
OLDERHARD18 分钟前
Java - MyBatis(上)
java·oracle·mybatis
杨荧19 分钟前
【JAVA开源】基于Vue和SpringBoot的旅游管理系统
java·vue.js·spring boot·spring cloud·开源·旅游
zaim12 小时前
计算机的错误计算(一百一十四)
java·c++·python·rust·go·c·多项式
hong_zc3 小时前
算法【Java】—— 二叉树的深搜
java·算法
进击的女IT4 小时前
SpringBoot上传图片实现本地存储以及实现直接上传阿里云OSS
java·spring boot·后端