Java Socket
1. 套接字介绍
套接字是介于传输层(TCP/UDP)和应用层(HTTP/FTP)之间的一个抽象,被应用程序调用;
在java环境中套接字编程主要使用TCP/IP协议,但是套接字支持的协议族远不止这些;
在java套接字编程中有Socket和ServerSocket两个核心类,ServerSocket位于服务器端监听连接,Socket位于客户端发起连接,服务器端监听到连接后也会产生一个Socket实例来完成与对端Socket的通信,并且服务器端的Socket和客户端的Socket没有任何区别,也就是说进程间通信需要一对套接字完成;
2. API介绍
Socket和ServerSocket是java提供网络编程的门面接口,实际上都是通过SocketImpl及子类完成的;
2.1 重要类关系图
2.2 重要类介绍
1. InetAddress
InetAddress是java对IP地址的封装,包括IP设备的名称、ip地址,有Inet4Address和Inet6Address两个子类;
没有公开的构造方法,只能通过公开的静态方法实例化对象
方法介绍
1)getByName:实例化一个InetAddress对象,getByName的参数可以是ip地址字符串,也可以是主机名
2)anyLocalAddress:实例化一个表示任何本地地址的网络地址,通常是0.0.0.0
3)getHostAddress一般通过InetAddress的获取主机ip地址串,通过获取主机名
4)getHostName
2. SocketAddress/InetSocketAddress
套接字地址,用来表示一个套接字在网络中的位置;
SocketAddress不依赖任何协议的套接字地址;
InetSocketAddress表示一个IP套接字地址,由套接字地址的 IP地址(InetAddress)、主机名和端口号组成
3. ServerSocket
服务器套接字,在服务器端用于监听网络传入
构造方法
1)ServerSocket():创建一个不绑定任何ip和端口的服务器套接字
创建一个SocksSocketImpl赋值给ServerSocket的SocketImpl,并把自己赋值给SocketImpl的ServerSocket
2)ServerSocket(port, backlog, InetAddress):用指定的端口、连接大小、ip地址创建一个服务器套接字
同无参构造,创建SocksSocketImpl并相互赋值
根据port和InetAddress创建套接字地址InetSocketAddress(InetAddress, port),如果InetAddress为null则通过InetAddress的anyLocalAddress实例化一个IP地址,如果port不合法则抛出异常;
通过调用bind(SocketAddress, backlog)将服务器套接字和套接字地址进行绑定;
backlog说明https://blog.csdn.net/oyueyang1/article/details/80451535
方法介绍
1)bind(SocketAddress, backlog):将服务器套接字和套接字地址进行绑定,tcp accept队列大小为backlog
如果SocketAddress为null则新建一个InetSocketAddress(InetAddress.anyLocalAddress, 0),0表示让系统随机分配端口号
实际上通过SocketImpl的bind(InetAddress, port)绑定的,SocketImpl是在构造函数中创建并绑定到当前类的
有参构造方法在构造过程都进行了bind操作,所以此方法一般与无参构造方法同时使用
2)isBound、getInetAddress、getLocalPort、getLocalSocketAddress查询bind方法的绑定信息
3)accept:阻塞式的监听到此服务器套接字连接,监听到连接后会创建一个Socket套接字,服务端和客户端各有一个Socket,2个套接字可以完成不同进程间通信;
4)implAccept(Socket):为保护方法,当我们自定义ServerSocket需要复写accept方法时,需要新建一个Socket然后通过implAccept加工,accept的阻塞其实是阻塞在implAccept的加工过程
5)getChannel:获取ServerSocketChannel通道
6)close/isClose:关闭套接字/判断套接字是否已关闭
关闭服务器套接字后,阻塞在accept的线程会抛出SocketException;
ServerSocketChannel通道关闭;
注意:没有直接方法判断对端套接字是否关闭
7)getSoTimeout/setSoTimeout: 设置accept的阻塞时间,0时表示无穷大,如果超时抛出SocketTimeOutException;
间接调用SocketImple的getOption(capId)/setOption(capId,Object), capId为SO_TIMEOUT(SocketOption中定义);
8)setSocketFactory(SocketImplFactory):设置套接字实现工厂
构造方法中会创建一个SocketImpl,默认是创建SocksSocketImpl,如果已设置SocketImplFactory则会调用工厂方法创建一个
4. Socket
套接字或客户端套接字,不同进程通信的端点,服务器端会有n多个客户端套接字
构造方法
1)Socket():创建一个不绑定任何套接字地址,不连接任务服务器套接字的套接字
创建一个SocksSocketImpl赋值给Socket的SocketImpl,并把自己赋值给SocketImpl的Socket, 同ServerSocket的无参构造方法
2)Socket(SocketAddress serverAddress, SocketAddress localAddress, boolean stream):创建套接并绑定和连接到指定套接字地址
绑定到本地套接字地址localAddress,连接的服务器套接字地址为serverAddress;
如果localAddress为null则内核随机选择一个本地ip地址和端口进行绑定;
stream表示流套接字(tcp),负责数据包datagram套接字(udp,每个包有大小限制)
3)Socket(InetAddress server, int serverPort, InetAddress local, int localPort):创建套接并绑定和连接到指定套接字地址
将server和serverPort封装成服务器套接字SocketAddress,local和localPort封装成客户端套接字SocketAddress,然后调用Socket(SocketAddress, SocketAddress, stream)创建流式套接字
4)Socket(String host, int port):
将host和port封装成服务器端套接字地址SocketAddress;
本地SocketAddress为null则,本地端口随机,任意一本机ip;
调用Socket(SocketAddress, SocketAddress, stream)创建流式Socket;
方法介绍
1)connect(SocketAddress)/connect(SocketAddress, timeout): 连接到套接字地址指定的服务器套接字
有参数构造方法中都会调用connect方法,所以此方法一般与无参构造方法同时使用
2)bind(SocketAddress) :将套接字和套接字地址进行绑定
如果已绑定则异常;
如果没有绑定在connect方法中会进行绑定,所以如果要调用bind方法需要在connect前调用;
3)getInetAddress()/getPort()/getRemoteSocketAddress(): 获取 对端 套接字的IP地址、端口号、套接字地址,没有连接则返回null
4)getLocalAddress()/getLocalPort()/getLocalSocketAddress():获 此 套接字的IP地址、端口号、套接字地址
5)getChannel():返回此套接字的SocketChannel通道
6)getInputStream():返回套接字的输入流
如果连接正常,inputstream的read会一直阻塞;
关闭输入流会关闭对应的套接字;
当Socket底层的连接中断时,套接字已缓存的字节可以通过read读取,如果已经消耗了所有缓存的字节,read操作会抛出IOException,套接字上没有任何缓存字节,并且套接字没有关闭调用 available返回0
7)getOutputStream():返回套接字的输出流
关闭输出流会关闭对应的套接字
8)setSoTimeout(timeout):Socket关联的InputStream的read操作默认是阻塞的,如果设置超时值,read操作超时引发SocketTimeoutException
实际上通过SocketImpl的setOption(optId)实现, optId为SO_TIMEOUT(SocketOptions中定义)
9)close:关闭套接字
阻塞于当前套接字的IO操作会抛出SocketException;
关闭后不能重新连接和重新绑定(已建立的连接不能用),只能通过新建;
会关闭此套接字的输入输出流;
关闭关联的通道;
10)shutdownInput:丢弃发送到输入流的所有字节,读取内容返回EOF
11)shutdownOutput:禁用输出流,以前发送的字节都会被底层发送,如果关闭后执行写入会抛出IOException
12)isBound/isClose/isConnect/isInputShutdown/isOutputSHutdown:用于判断相应的操作是否结束
13)setSocketImplFactory:设置套接字实现的工厂,构造方法默认会创建一个SocksSocketImpl,如果指定工厂会通过工厂创建
3. 实例
工具类:SocketUtil
1 import java.net.InetSocketAddress; 2 import java.net.SocketAddress; 3 4 /** 5 * 工具类 6 */ 7 public class SocketUtil { 8 9 /** 10 * 解析套接字地址中的ip和端口 11 */ 12 public static String getSocketAddress(SocketAddress socketAddress) { 13 14 if (socketAddress instanceof InetSocketAddress) { 15 InetSocketAddress inetSocketAddress = ((InetSocketAddress) socketAddress); 16 int port = inetSocketAddress.getPort(); 17 String hostName = inetSocketAddress.getHostName(); 18 return String.format("%s:%s", hostName, port); 19 } 20 21 return null; 22 } 23 }
服务器类:DemoTcpServer
1 import java.io.*; 2 import java.net.*; 3 import java.util.*; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 7 /** 8 * 群聊服务器:启动服务器套接字,监听并处理连接 9 */ 10 public class DemoTcpServer { 11 12 static final List<Socket> ONLINE_SOCKET = new ArrayList(); 13 14 public static void main(String[] args) throws IOException { 15 16 // 创建服务器套接字,并绑定到本机的41440端口监听 17 ServerSocket serverSocket = new ServerSocket(41440, 50, InetAddress.getByName("192.168.1.101")); 18 System.out.println("服务器started..."); 19 20 // 创建线程池用于处理已连接的socket 21 ExecutorService executorService = Executors.newFixedThreadPool(10); 22 while (true) { 23 // 等待socket接入,有连接接入后会创建一个Socket与对端Socket通信,此Socket地址ip为本机、端口随机 24 Socket socket = serverSocket.accept(); 25 String socketAddress = SocketUtil.getSocketAddress(socket.getRemoteSocketAddress()); 26 System.out.println(socketAddress + " 上线了..."); 27 // 加入到在线的socket列表,用于群发消息 28 ONLINE_SOCKET.add(socket); 29 // 单独起一个线程监听消息,可以改成nio的Selectors实现 30 executorService.submit(new SocketHandler(socket)); 31 } 32 } 33 } 34 35 /** 36 * 处理Socket的任务 37 */ 38 class SocketHandler implements Runnable { 39 40 private Socket socket; 41 42 public SocketHandler(Socket socket) { 43 this.socket = socket; 44 } 45 46 @Override 47 public void run() { 48 try { 49 String clientFlag = SocketUtil.getSocketAddress(this.socket.getRemoteSocketAddress()); 50 byte[] input = new byte[1024]; 51 // 循环监听消息 52 while (true) { 53 // 阻塞式读取消息 54 int read = this.socket.getInputStream().read(input); 55 // 服务器记录发送的所有消息 56 System.out.println(clientFlag + " " + new String(input, 0, read, "utf-8")); 57 // 群发消息 58 sendMsg(this.socket, clientFlag, new String(input, 0, read, "utf-8")); 59 } 60 } catch (IOException e) { 61 System.out.println(SocketUtil.getSocketAddress(socket.getRemoteSocketAddress()) + "下线..."); 62 DemoTcpServer.ONLINE_SOCKET.remove(socket); 63 } 64 } 65 66 /** 67 * 群发消息 68 */ 69 private void sendMsg(Socket ignoreSocket, String clientFlag, String msg) { 70 71 Iterator<Socket> iterator = DemoTcpServer.ONLINE_SOCKET.iterator(); 72 while (iterator.hasNext()) { 73 74 Socket currentSocket = iterator.next(); 75 76 // 不需要向此Socket对端发送消息,只有一个客户端时需要注解此判断 77 if (currentSocket.equals(ignoreSocket)) { 78 continue; 79 } 80 81 // 发送消息到客户端 82 try { 83 currentSocket.getOutputStream().write((clientFlag + System.lineSeparator() + msg).getBytes("utf-8")); 84 } catch (Exception e) { 85 System.out.println(SocketUtil.getSocketAddress(currentSocket.getRemoteSocketAddress()) + "下线..."); 86 iterator.remove(); 87 } 88 } 89 } 90 }
客户端类:
1 import java.io.IOException; 2 import java.net.InetSocketAddress; 3 import java.net.Socket; 4 import java.util.Scanner; 5 6 /** 7 * 客户端 8 */ 9 public class DemoTcpClient { 10 public static void main(String[] args) throws Exception { 11 12 //以下3步可以通过有参构造方法一次指定 13 // 创建客户端socket 14 Socket socket = new Socket(); 15 // 绑定到本地的ip和端口号 16 socket.bind(new InetSocketAddress("192.168.1.101", 41441)); 17 // 连接到远程的套接字服务器 18 socket.connect(new InetSocketAddress("192.168.1.101", 41440), 0); 19 System.out.println("已上线..."); 20 21 // 新启动线程用于回显消息 22 new Thread(() -> { 23 try { 24 byte[] input = new byte[1024]; 25 while (true) { 26 int read = socket.getInputStream().read(input); 27 System.out.println(new String(input, 0, read, "utf-8")); 28 } 29 } catch (IOException e) { 30 e.printStackTrace(); 31 } 32 }).start(); 33 34 //处理输入 35 Scanner scanner = new Scanner(System.in); 36 while (scanner.hasNext()) { 37 socket.getOutputStream().write(scanner.nextLine().getBytes("utf-8")); 38 socket.getOutputStream().flush(); 39 } 40 } 41 }
参考文献:
1. https://droidyue.com/blog/2015/03/08/sockets-programming-in-java/
2. https://my.oschina.net/leejun2005/blog/104955
3. https://www.cnblogs.com/w-wfy/p/6415840.html