介绍了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套接字有如下分类:
- 流式套接字(SOCK_STREAM):流式套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流式套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP协议。
- 数据报套接字(SOCK_DGRAM):数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据,但是传输效率较高。数据报套接字使用UDP协议进行数据的传输。
- 原始套接字:原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。
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学习博客!