介绍了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. 原始套接字:原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。

2 Socket通信

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

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

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

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

因此,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如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地址组成的Inet…

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学习博客!