Java实现简易聊天室以及Scoket编程入门

介绍了Scoekt的概念,并且提供了基于TCP和UDP协议的Java Socket API编写的简单通信程序,比如简易的聊天室。

此前我们简单的学了各种协议,我们知道大部分的应用层协议,比如HTTP、FTP、SMTP、POP3等,它们都依赖于下层传输层的TCP/UDP协议进行数据传输,因此实际上我们可以直接使用TCP/UDP协议进行网络通信,并且大部分语言都已经提供了现成的一套TCP/UDP编程API,那就是Scoket。下面简单的学习可以不依赖于应用层协议进行网络通信的Socket编程。

1 Socket概述

Socket翻译成中文就是套接字。它是对TCP/IP协议包括下层各种协议的封装,Socket本身并不是协议,而是一个接口(API),它只是提供了一个针对TCP/UDP编程的接口它将复杂的TCP/UDP协议的各种操作隐藏起来,我们只需要遵循Socket的开发规定去编程,写成的程序自然遵循TCP/UDP协议,自然就能够是进行两台计算机相互通信,这类似于设计模式中的门面模式!

即,Socket隐藏了各种TCP/IP协议的交互细节,提供了需要针对TCP/UDP编程的各种高级语言的上层接口,可以接收请求和发送响应,实现不同计算机之间的通信。实际上,底层协议本来就提供了可以进行网络编程的接口,但是太底层了,对于很多程序员不友好,特别是使用Java等上层语言的程序员,因此,出现了Scoket接口以及它的相关API,Scoket对于这些底层协议的接口进行了进一步封装,并且通过高级语言的API开放出来,对于需要进行网络编程的普通程序员来说更加友好。

不同的语言都提供了Socket API实现,Java也有,比如java.net.Socket、java.net.DatagramSocket等类。

Java的Socket套接字有如下分类:

  1. 流式套接字(SOCK_STREAM):流式套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流式套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP协议。
  2. 数据报套接字(SOCK_DGRAM):数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据,但是传输效率较高。数据报套接字使用UDP协议进行数据的传输。
  3. 原始套接字:
    1. 流式套接字和数据报套接字这两种套接字工作在传输层,主要为应用层的应用程序提供服务,并且在接收和发送时只能操作数据部分,而不能对IP首部或TCP和UDP首部进行操作,通常把这两种套接字称为标准套接字。
    2. 原始套接字工作在网络层,主要用于一些更底层的协议的开发,可以进行比较底层的操作,比如发送一个自定义的IP包、UDP包、TCP包或ICMP包。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。原始套接字可以在链路层收发数据帧。

2 Socket通信

Socket包含了进行网络通信必需的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

Socket是对TCP/IP协议的封装,因此,根据我们前面学习的通信协议,最简单的Socket编程并没有使用上层——应用层的相关协议,比如HTTP、SMTP、POP3等等。因为我们在传输数据时,可以只使用传输层及其以下的协议,而不使用应用层的协议。

如果Socket是使用TCP协议进行通信,那么Socket程序同样会涉及到TCP连接的三次握手和四次挥手,基于TCP的Socket通信流程图如下:

在这里插入图片描述

虽然我们可以直接使用Socket进行通信,可以互相传输到数据,但是Socket传输或者接收的数据都是的byte字节数据,如果没有应用层及其相关协议,我们无法识别传递的字节数据对应的原始数据本身的类型、内容,格式等等,这样就无法将byte字节数据还原成原始的数据。

因此,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,它们用于传输不同的数据,为不同的应用服务。我们的Web应用更多的是使用HTTP协议作应用层协议,以封装HTTP报文信息,然后使用TCP/IP做传输层协议将它发到服务器或者客户端。

上层应用程序/协议的通信需要依靠的下层的协议,如果我们需要使用应用层协议进行通讯。那么该怎么办呢?实际上,Socket已经提供了对接上层应用层协议的接口,并且JDK已经为我们提供好了对于上层协议的封装类,比如HttpURLConnection、URL、HttpClient等等基于HTTP协议封装的API,又比如基于FTP协议封装的FtpClient工具类,又比如基于SMTP协议封装的SmtpClient工具类……。它们的底层基本上最终还是调用了JDK的Socket的API进行数据传输,这些应用层协议的工具类,需要做的就是将数据编码通过Socket传输,或者将接收到的数据以自身指定的格式解码。

实际上,就目前而言,几乎所有的语言的Web应用程序的底层都是采用Socket进行通信的。

3 使用UDP协议通信

使用UDP协议进行数据的传输,主要是用到两个类,一个是DatagramSocket,另一个就是DatagramPacket。

3.1 相关类

3.1.1 InetAddress ip地址的类

public class InetAddress
extends Object
implements Serializable

位于java.net包当中,此类表示互联网协议 (IP) 地址的抽象,InetAddress对象封装了ip地址。
IP 地址是IP协议使用的32位或128位无符号数字,IP协议是一种更加低级协议,UDP 和 TCP 协议都是在它的基础上构建的。
无构造方法。

3.1.1.1 获得InetAddress对象

public static InetAddress getLocalHost()

返回本地主机的IP地址。获得InetAddress对象。由主机名/ip地址组成。比如:DESKTOP-8Q842HN/192.168.253.1

public static InetAddress getByName(String host)

在给定主机名的情况下确定主机的 IP 地址。获得InetAddress对象。

主机名可以传递机器名如”DESKTOP-8Q842HN”,返回由 机器名/ip地址组成的InetAddress对象:ESKTOP-8Q842HN/192.168.253.1

也可以是传入IP地址的文本表示形式如”192.168.253.1”,返回由/ip地址组成的InetAddress对象。但是任然可以使用该对象通过方法获得主机名:/192.168.253.1

还可以传入域名,例如”www.baidu.com”。返回由域名/ip地址组成的InetAddress对象:www.baidu.com/119.75.217.109

3.1.1.2 获得本机Ip和主机名
String getHostAddress()获得String类型的ip地址。返回字符串格式的原始 IP 地址。
String getHostName()返回此 IP 地址的主机名;如果安全检查不允许操作,则返回 IP 地址的文本表示形式。
String toString()返回的字符串具有以下形式:主机名/字面值IP地址。

另外,还有一个InetSocketAddress类,它表示IP 套接字地址(IP 地址 + 端口号),它还可以是一个对(主机名 + 端口号),在此情况下,将尝试解析主机名。如果解析失败,则该地址将被视为未解析地址,但是其在某些情形下仍然可以使用,比如通过代理连接。

3.1.2 DatagramSocket 数据报套接字类

public class DatagramSocket
extends Object

位于java.net包。此类表示用来发送和接收数据报包的套接字,又称数据报套接字。数据报套接字是包投递服务的发送或接收点,使用UDP协议发送数据包。

3.1.2.1 构造器
DatagramSocket()构造数据报套接字并将其绑定到本地主机上任何可用的端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。
DatagramSocket(int port)
port -表示要使用的端口。
创建数据报套接字并将其绑定到本地主机上的指定端口。
3.1.2.2 API方法
void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区已经填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。此方法在接收到数据报包前一直阻塞。
InetAddress getLocalAddress()返回套接字绑定的本地地址,如果套接字没有绑定则返回表示任何本地地址的InetAddress。
int getPort()返回此套接字的端口。如果套接字未连接,则返回 -1。

3.1.3 DatagramPacket 数据报包类

public final class DatagramPacket
extends Object

此类表示数据报包。数据报包用来实现无连接包投递服务。发送的多个数据报包不能保证达到顺序也不能保证完整性。发送次数必须和接收次数一致,否则将会丢失数据。

1.3.1 构造器

DatagramPacket(byte[] buf,int offset,int length,InetAddress address,int port);
buf - 包数据。
offset - 包数据偏移量。
length - 包数据长度。
address - 目的地址。
port - 目的端口号。
构造数据报包,用来将长度为length偏移量为offset的包发送到指定主机上的指定端口号。length参数必须小于等于buf.length。
DatagramPacket(byte[] buf,int length)
buf - 保存传入数据报的缓冲区。
len - 要读取的字节数。
构造 DatagramPacket,用来接收长度为length的数据包。length参数必须小于等于buf.length。
3.1.3.2 API方法
InetAddress getAddress()返回某台机器的IP地址,此数据报将要发往该机器或者是从该机器接收到的。
int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或者是从该主机接收到的。
byte[] getData()返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续 length 长度。
int getLength()返回将要发送或接收到的数据的长度。
int getOffset()返回将要发送或接收到的数据的偏移量。

3.2 基本案例

3.2.1 UDP发送端

public class Sender {
    public static void main(String[] args) throws IOException, IOException {
        //1.创建发送端数据报包套接字socket,用来发送数据报包。如果指定一个端口号,指定的是发送端的端口号;如果不指定端口号,系统会默认分配一个。
        DatagramSocket ds = new DatagramSocket();
        //2.构造数据报包,包括:发送的数据的字节数组,起始索引,数据长度,指定远程主机的ip地址[InetAddress对象],以及远程主机上的端口号.(这里就发送到本机演示)
        DatagramPacket dp = new DatagramPacket("你好".getBytes(), 0, "你好".getBytes().length, InetAddress.getLocalHost(), 8888);
        //3.发送数据报包
        ds.send(dp);
        //4.关闭套接字socket
        ds.close();
    }
}

3.2.2 UDP接收端

public class Receiver {
    public static void main(String[] args) throws IOException {
        //创建接收端数据报包套接字socket,必须指定接收端端口号
        DatagramSocket ds = new DatagramSocket(8888);
        while (true) {    //循环接收数据
            //构造空数据报包,用来接收数据:内部使用字节数组作为接收数据的缓冲区,可以指定起始索引和要读取的字节数.
            //如果发送的数据量大于接收空间的大小,那么数据将会丢失
            byte[] by = new byte[1024];
            DatagramPacket dp = new DatagramPacket(by, 0, by.length);
            //接收数据:将数据存入数据报包中.在接收到数据前,此方法将一直堵塞!
            ds.receive(dp);
            //打开数据报包,获得数据缓冲区数组,这里将会获取一次发送的全部数据
            byte[] data = dp.getData();
            //dp.getLength(),是指的接收到的数据的长度。
            System.out.println("data: " + new String(data, 0, dp.getLength()));
            //获得发送端ip地址
            String hostName = dp.getAddress().getHostName();
            System.out.println("hostName: " + hostName);
            //获得发送端主机名
            String hostAddress = dp.getAddress().getHostAddress();
            System.out.println("hostAddress: " + hostAddress);
            //获得发送端端口号
            int port = dp.getPort();
            System.out.println("port: " + port);
        }
        //ds.close(); 接收端应该一直开着接收数据
    }
}

3.3 UDP实现简易的聊天室

接收服务:

public class ReceiveServer implements Runnable {
    private final DatagramSocket dsReceive;

    public ReceiveServer(DatagramSocket dsReceive) {
        this.dsReceive = dsReceive;
    }

    @Override
    public void run() {
        while (true) {
            //构造空数据报包,用来接收数据:内部使用字节数组作为接收数据的缓冲区,可以指定起始索引和要读取的字节数.
            byte[] by = new byte[1024];
            DatagramPacket dp = new DatagramPacket(by, 0, by.length);
            //接收数据:将数据存入数据报包中.在接收到数据前,此方法将一直堵塞!
            try {
                dsReceive.receive(dp);
            } catch (IOException e) {
                e.printStackTrace();
            }
            //打开数据报包,获得数据缓冲区数组
            byte[] byteData = dp.getData();
            //dp.getLength(),是指的接收到的数据的长度。
            String data = new String(byteData, 0, dp.getLength());
            //获得时间
            String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            //获得发送端主机IP
            String hostAddress = dp.getAddress().getHostAddress();
            //获得发送端口号
            int port = dp.getPort();
            System.out.println(time + "---" + hostAddress + ": " + port);
            System.err.println(data);
        }
    }
}

发送服务:

public class SendServer implements Runnable {

    private final DatagramSocket dsSend;
    private final InetSocketAddress inetSocketAddress;

    public SendServer(DatagramSocket dsSend, InetSocketAddress inetSocketAddress) {
        this.dsSend = dsSend;
        this.inetSocketAddress = inetSocketAddress;
    }

    @Override
    public void run() {
        try {
            //接收键盘录入的数据
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            String str;
            while ((str = br.readLine()) != null) {
                //构造数据报包,包括:发送的数据的字节数组,起始索引,长度,指定远程ip,以及远程ip上的端口号.
                DatagramPacket dp = new DatagramPacket(str.getBytes(), 0, str.getBytes().length, inetSocketAddress);
                //发送数据报包
                dsSend.send(dp);
                //定义结束语句
                if (str.equals("886")) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭套接字socket
            dsSend.close();
        }
    }
}

客户端1:

public class ChatClient1 {
    public static void main(String[] args) throws SocketException, UnknownHostException {
        //发送服务,发送到指定Ip和端口的接收服务中。这里的ip就是本机,端口为9999
        String hostName = InetAddress.getLocalHost().getHostName();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(hostName, 9999);
        SendServer st = new SendServer(new DatagramSocket(), inetSocketAddress);
        //接收服务,接收端口号为9999
        ReceiveServer rt = new ReceiveServer(new DatagramSocket(8888));
        new Thread(st).start();
        new Thread(rt).start();
    }
}

客户端2:

public class ChatClient2 {
    public static void main(String[] args) throws SocketException, UnknownHostException {
        //发送服务,发送到指定Ip和端口的接收服务中。这里的ip就是本机,端口为8888
        String hostName = InetAddress.getLocalHost().getHostName();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(hostName, 8888);
        SendServer st = new SendServer(new DatagramSocket(), inetSocketAddress);
        //接收服务,接收端口号为9999
        ReceiveServer rt = new ReceiveServer(new DatagramSocket(9999));
        new Thread(st).start();
        new Thread(rt).start();
    }
}

4 使用TCP协议通信

注意:使用TCP传输,一定要先开启服务器,因为传输的数据一定要保证被收到,否则抛出异常。而UDP本来就不保证数据被收到,因此即使先开启了发送端,也不会报错!

使用TCP协议进行数据的传输,主要是用Socket和ServerSocket,以及输入、输出流!

4.1 相关类

4.1.1 Socket套接字类

public class Socket
extends Object

Java中的Socket类,专门用于TCP请求。

套接字是两台机器间通信的端点。此类实现客户端套接字,并且该类套接字是基于TCP协议的,数据是通过流传输的,因此又称流套接字。网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标识符套接字。

通信的两端都有Socket,网络通信其实就是Socket间的通信,数据在两个Socket间通过IO流传输。

在这里插入图片描述

4.1.1.1 构造器
Socket(String host,int port)
host - 主机名(字符串类型的IP地址),表示服务端字符串IP。
port - (应用程序)端口号。
创建一个流套接字并将其连接到指定主机上的指定(应用程序)端口号。
Socket(InetAddress address,int port)
address – InetAddress类的服务端IP 地址。
port - (应用程序)端口号。
创建一个流套接字并将其连接到指定IP地址的指定端口号。
4.1.1.2 API方法
OutputStream getOutputStream()返回此套接字的输出流。
InputStream getInputStream()返回此套接字的输入流。如果未读取到对方发送的数据,此方法将一直阻塞。
InetAddress getInetAddress()返回套接字连接的地址。
InetAddress getLocalAddress()获取套接字绑定的本地地址。
int getPort()返回此套接字连接到的远程端口。
int getLocalPort()返回此套接字绑定到的本地端口。
void shutdownInput()此套接字的输入流置于“流的末尾”。发送到套接字的输入流端的任何数据都将被确认然后被静默丢弃。
void shutdownOutput()禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。
void close()关闭此套接字。

4.1.2 ServerSocket 服务器套接字类

public class ServerSocket
extends Object

此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。

构造器:

ServerSocket(int port)创建绑定到特定端口的服务器(应用程序)套接字。

API方法:

Socket accept()侦听并接受到此套接字的连接。此方法在成功被客户端连接并返回之前在一直阻塞,将返回客户端套接字,通过该返回的客户端套接字可以接收客户端发送过来的数据,或者给客户端发送响应信息
void close()关闭此套接字。

4.2 基本案例

服务器接收到客户端的数据,然后,响应给客户端一个数据。

4.2.1 TCP服务端

public class Server {
    public static void main(String[] args) throws IOException {
        //1.创建服务器socket,并绑定端口号
        ServerSocket ss = new ServerSocket(8888);
        //2.监听客户端连接,返回对应的socket对象.此方法在成功被客户端连接并返回之前一直阻塞!
        Socket a = ss.accept();
        //获得客户端主机名,ip地址,端口
        InetAddress ia = a.getInetAddress();
        System.out.println("client :" + ia.getHostAddress() + ": " + a.getPort());
        //创建输入流,使用read()读取数据,如果未读取到对方发送的数据,此方法将一直阻塞!
        InputStream is = a.getInputStream();
        byte[] b = new byte[1024];
        int read = is.read(b);
        System.out.println("from client: " + new String(b, 0, read));


        //给客户端响应,获得输出流,发送数据
        OutputStream os = a.getOutputStream();
        os.write("已经收到".getBytes());
        os.flush();
        //释放获得的socket资源,服务器socket不应该关闭
        a.close();
    }
}

4.2.2 TCP客户端

public class Client {
    public static void main(String[] args) throws IOException {
        //1.创建客户端socket,并将其连接到指定 IP 地址的指定端口号。
        Socket s = new Socket(InetAddress.getLocalHost(), 8888);
        //等待4秒再发送消息
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(4));
        //2.获得输出流,写数据
        OutputStream os = s.getOutputStream();
        os.write("你好,收到数据了吗?".getBytes());
        os.flush();
        //socket.shutdownOutput();   服务端循环接收时,需要用此方法关闭
        //获得输入流,使用read()读取服务器的响应,在读取到数据之前,此方法一直阻塞!
        InputStream is = s.getInputStream();
        byte[] by = new byte[1024];
        int read;
        while ((read = is.read(by)) != -1) {
            System.out.println("from server: " + new String(by, 0, read));
        }
        //关闭客户端,释放资源
        s.close();
    }
}

4.3 文本上传

客户端:上传一个文本,服务端:保存起来并响应!

客户端:

public class TxtClient {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket(InetAddress.getLocalHost(), 8888);
        //文本读入流,读取文本所在的位置
        BufferedReader br = new BufferedReader(new FileReader("E:\\Idea\\Java-EE\\WebProgram\\src\\main\\resources\\a.txt"));
        //客户端输出流
        OutputStream os = s.getOutputStream();
        PrintWriter pw = new PrintWriter(os);
        String str;
        while ((str = br.readLine()) != null) {
            //将文本数据写入到客户端输出流中,发送给服务器
            pw.println(str);
            pw.flush();
        }
        s.shutdownOutput();
        /*
         * 接收服务器响应
         */
        //客户端输入流
        InputStream is = s.getInputStream();
        BufferedReader br1 = new BufferedReader(new InputStreamReader(is));
        String str1;
        while ((str1 = br1.readLine()) != null) {
            System.out.println(str1);
        }
        s.close();
    }
}

服务器:

public class TxtServer {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8888);
        Socket a = ss.accept();
        //文件输出流,指定上传的文件名和路径
        PrintWriter pw = new PrintWriter("E:\\Idea\\Java-EE\\WebProgram\\src\\main\\resources\\b.txt");
        //服务端输入流
        InputStream is = a.getInputStream();
        //转换为缓冲流
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String str;
        System.out.println("文件内容为:");
        while ((str = br.readLine()) != null) {
            System.out.println(str);
            //服务器将数据保存到指定的地方
            pw.println(str);
            pw.flush();
        }
        /*
         * 给客户端响应
         */
        //服务端输出流
        PrintWriter pw1 = new PrintWriter(a.getOutputStream());
        pw1.println("-----------");
        pw1.println("文件已上传");
        pw1.flush();
        a.close();
    }
}

4.4 图片上传

服务端使用多线程技术处理客户端请求,防止同时出现多个客户端的请求时发生阻塞。

客户端:

public class PicClient {
    public static void main(String[] args) throws IOException {
        String filePath = "E:\\Idea\\Java-EE\\WebProgram\\src\\main\\resources\\QQ图片20201123093907.png";
        savePic(filePath);
    }

    static void savePic(String filePath) throws IOException {
        File pictureFile = new File(filePath);
        if (!pictureFile.exists()) {
            System.out.println("你上传的文件不存在");
            return;
        }
        if (!pictureFile.isFile()) {
            System.out.println("你上传的不是一个文件");
            return;
        }
        if (!pictureFile.getName().endsWith(".jpg")) {
            System.out.println("你上传的不是一个jpg格式文件");
            return;
        }
        if (pictureFile.length() > 1024 * 1024 * 3) {
            System.out.println("上传图片大小超过限制,最大不超过3M");
            return;
        }
        //创建客户端
        Socket s = new Socket(InetAddress.getLocalHost(), 8888);
        //准备一个输入流,用来读取图片
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(pictureFile));
        //准备一个客户端输出流用来传输数据
        BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
        //准备一个客户端输入流用来接收服务端的响应数据
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        //发送图片数据
        byte[] by = new byte[1024];
        int len;
        while ((len = bis.read(by)) != -1) {
            bos.write(by, 0, len);
            bos.flush();
        }
        s.shutdownOutput();
        //接收服务端响应
        String readLine = br.readLine();
        System.out.println(readLine);
    }
}

服务端:

public class PicServer implements Runnable {

    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8888);
        while (true) {
            //accept方法在传入链接之前一直堵塞,因此不会无限循环
            Socket a = ss.accept();
            //使用多线程处理客户端连接,防止客户端的请求阻塞
            THREAD_POOL_EXECUTOR.submit(new PicServer(a));
        }
    }


    private Socket socket;

    public PicServer(Socket socket) {
        this.socket = socket;
    }

    static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    @Override
    public void run() {
        //客户端已连接反馈
        System.out.println(socket.getInetAddress().getHostName() + "已连接");
        //文件命名uuid
        String fileName = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
        try {
            //客户端输入流
            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            //文件输出流
            BufferedOutputStream bos = new BufferedOutputStream(
                    new FileOutputStream(("E:\\Idea\\Java-EE\\WebProgram\\src\\main\\resources\\" + fileName + ".jpg")));
            byte[] b = new byte[1024];
            int read;
            while ((read = bis.read(b)) != -1) {
                bos.write(b, 0, read);
                bos.flush();
            }
            OutputStream os = socket.getOutputStream();
            //客户端输出流
            PrintWriter pw = new PrintWriter(os);
            pw.println(socket.getLocalAddress().getHostName() + "文件已上传");
            pw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4.5 TCP实现简易的多人聊天室

要求先启动服务端,然后启动多个客户端。录入的消息格式为“name:message”,name为指定的其他用户名,用于私聊,name为all的时候表示发送群聊!

服务端:

public class ChatServer {

    /**
     * 服务器保存所有的用户
     */
    private static HashSet<User> users = new HashSet<>();


    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);

        //循环处理连接
        while (true) {
            //一个连接表示一个用户
            Socket accept = serverSocket.accept();
            User user = new User(accept);
            users.add(user);
            new Thread(user).start();
        }

    }

    /**
     * 一个连接表示一个用户,并且能够转发消息
     */
    private static class User implements Runnable {

        public User() {
        }

        //记录连接用户的名字
        private String name;

        public String getName() {
            return name;
        }

        //负责接收
        private DataInputStream is;
        //负责发送
        private DataOutputStream os;

        public User(Socket client) throws IOException {
            is = new DataInputStream(client.getInputStream());
            os = new DataOutputStream(client.getOutputStream());
            name = is.readUTF();
            this.send("欢迎 " + name + " 进入聊天室", true, false);
            this.send("您已经进入了聊天室", true, true);
        }

        /**
         * 接收消息,随后转发到对应的用户
         */
        @Override
        public void run() {
            while (true) {
                try {
                    this.send(this.revice(), false, false);
                } catch (IOException e) {
                    users.remove(this);
                    try {
                        is.close();
                    } catch (IOException ioException) {
                        ioException.printStackTrace();
                    }
                    try {
                        os.close();
                    } catch (IOException ioException) {
                        ioException.printStackTrace();
                    }
                    e.printStackTrace();
                }
            }
        }

        //接收信息
        public String revice() throws IOException {
            return is.readUTF();
        }


        /**
         * 发送消息
         *
         * @param msg       原始消息
         *                  如果非系统用户,那么普通用户的原始消息以 name:msg 的方式发送,name为all表示向全部在线用户发送
         * @param system    是否是系统消息
         * @param isPrivate 是否是私聊
         */
        public void send(String msg, boolean system, boolean isPrivate) throws IOException {
            if (system) {
                if (isPrivate) {
                    send("系统" + ":" + isPrivate + ":" + msg);
                    return;
                }
                for (User client : users) {
                    client.send("系统" + ":" + isPrivate + ":" + msg);
                }
            } else {
                if (msg.contains(":")) {
                    String[] split = msg.split(":");
                    if ("all".equals(split[0])) {
                        for (User client : users) {
                            if (client != this) {
                                client.send(this.name + ":" + isPrivate + ":" + split[1]);
                            }
                        }
                    } else {
                        for (User user : users) {
                            if (user.getName().equals(split[0])) {
                                user.send(this.name + ":" + !isPrivate + ":" + split[1]);
                            }
                        }
                    }
                }
            }
        }

        /**
         * 发送信息
         *
         * @param msg 最终消息,格式为  name:isPrivate:msg
         */
        public void send(String msg) throws IOException {
            os.writeUTF(msg);
            os.flush();
        }
    }
}

客户端:

public class ChatClient {

    Socket socket;
    DataInputStream dataInputStream;
    DataOutputStream dataOutputStream;

    public ChatClient(Socket socket, String name) throws IOException {
        this.socket = socket;
        dataInputStream = new DataInputStream(socket.getInputStream());
        dataOutputStream = new DataOutputStream(socket.getOutputStream());

        new Thread(new SendServer(name)).start();
        new Thread(new ReceiveServer()).start();
    }

    /**
     * 客户端收取消息
     */
    class ReceiveServer implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    String data = dataInputStream.readUTF();
                    //获得时间
                    String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                    //获得发送端口号
                    String[] strs = data.split(":");
                    String type = "消息";
                    if ("true".equals(strs[1])) {
                        type = "私聊";
                    }
                    System.out.println(time + "---" + "来自 " + strs[0] + " 的" + type);
                    System.out.println("\t" + strs[2]);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 客户端发送消息
     */
    class SendServer implements Runnable {

        public SendServer(String name) throws IOException {
            send(name);
        }

        @Override
        public void run() {
            try {
                //接收键盘录入的数据
                BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
                String str;
                while ((str = br.readLine()) != null) {
                    dataOutputStream.writeUTF(str);
                    dataOutputStream.flush();
                    //定义结束语句
                    if (str.contains("886")) {
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                //关闭套接字socket
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        public void send(String msg) throws IOException {
            dataOutputStream.writeUTF(msg);
        }
    }
}

测试客户端:

public class ChatTest {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
        new ChatClient(socket, "ChatClient1");
    }

    public static class ChatClient2 {
        public static void main(String[] args) throws IOException {
            Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
            new ChatClient(socket, "ChatClient2");
        }
    }

    public static class ChatClient3 {
        public static void main(String[] args) throws IOException {
            Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
            new ChatClient(socket, "ChatClient3");
        }
    }
}

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值