java.net.Socket类是Java完成客户端TCP操作的基础类。其他建立TCP网络连接的面向客户端的类如URL、URLConnection等最终都会调用这个类的方法。这个类本身使用原生代码与主机操作系统的本地TCP栈进行通信。
Java中的Socket类是对客户端Socket的抽象,另外还有服务端Socket,是ServerSocket类,我们这里会一起讲到。
客户端Socket
构造Socket
Socket表示连接,那么肯定要指定连接的ip和端口号,Socket提供了几种构造方法:
我们来看其中最容易理解的一个:
public Socket(String host, int port)
throws UnknownHostException, IOException
{
this(host != null ? new InetSocketAddress(host, port) :
new InetSocketAddress(InetAddress.getByName(null), port),
(SocketAddress) null, true);
}
这个构造方法就是提供一个host和一个端口号,然后它调用的是另外一个构造方法,我们从这可以看出,它使用host和端口号初始化了InetSocketAddress,InetSocketAddress代表一个IP地址(包括ip地址和端口号)。我们来看下这个构造方法的逻辑:
private Socket(SocketAddress address, SocketAddress localAddr,
boolean stream) throws IOException {
setImpl();
// backward compatibility
if (address == null)
throw new NullPointerException();
try {
createImpl(stream);
if (localAddr != null)
bind(localAddr);
connect(address);
} catch (IOException | IllegalArgumentException | SecurityException e) {
try {
close();
} catch (IOException ce) {
e.addSuppressed(ce);
}
throw e;
}
}
它接收的第一个SocketAddress表示要连接的地址,第二个参数表示本地地址,也就是说我们可以指定从本地的哪个ip和哪个端口号去连接,不过一般情况下我们不需要指定。至于它的实现逻辑,就不去看了。
从Socket中读取数据
建立了Socket连接之后,我们就可以从Socket中获取数据了。这个很简单,因为数据就是一个输入流,我们可以直接通过它的getInputStream方法来得到这个输入流,然后解析其中的数据。
下面是一个示例,time.nist.gov这个host的13端口会返回一个可读的当前时间,我们可以用telnet来获取它:
如果通过socket来实现,也比较简单:
public static void main(String[] args) throws IOException {
Socket socket = new Socket("time.nist.gov",13);
System.out.println(socket);
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder stringBuilder = new StringBuilder();
int c;
while((c=bufferedReader.read())!=-1){
stringBuilder.append((char)c);
}
System.out.println(stringBuilder);
}
只要获取socket,然后拿到输入流,然后解析其中的数据即可。
59697 22-04-28 11:41:17 50 0 0 305.5 UTC(NIST) *
注意我们使用socket的InputStream拿到的是TCP的数据:
也就是TCP包里面除了首部之外的数据。所以我们要想知道解析这个数据,就需要知道它携带的数据的格式,然后根据格式来正确解析它。例如,如果要发送和接收HTTP请求数据,从InputStream中获取到的数据就会包含HTTP的header和HTTP的用户数据。
写入Socket
除了读取数据,Socket同样支持写入数据,并且实现也很简单,只要获取OutputStream即可:
public static void main(String[] args) throws IOException {
try(Socket socket = new Socket("time.nist.gov",13)){
OutputStream outputStream = socket.getOutputStream();
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("hello");
}
}
与读取数据一样,我们需要知道服务端需要怎样的数据格式,才能按照这种格式写入。
服务端Socket
客户端请求数据要被服务端处理,我们可以使用ServerSocket来构造一个服务端Socket来接收并处理客户端的请求。
构造服务端Socket
要想构造一个ServerSocket,最少得需要一个指定一个要绑定的端口。ServerSocket类提供了下面这些构造方法:
我们来看其中最简单的一个:
public ServerSocket(int port) throws IOException {
this(port, 50, null);
}
一般来说,最简单的构造方法肯定是通过调用其他构造方法来实现的,这个也不例外,我们来看:
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
setImpl();
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException(
"Port value out of range: " + port);
if (backlog < 1)
backlog = 50;
try {
bind(new InetSocketAddress(bindAddr, port), backlog);
} catch(SecurityException e) {
close();
throw e;
} catch(IOException e) {
close();
throw e;
}
}
这个构造方法有三个参数,第一个是要绑定的本地端口,如果是0,则会自动锁定一个端口,第二个是请求连接的最大数量,默认是50,第三个InetAddress表示从本地的哪个ip接收请求,如果为null,表示接收本地任意地址的请求。
理解了这个构造方法,其他的就简单了,因为其他构造方法都是调用这个方法来实现的。
接收请求
ServerSocket创建成功之后,我们需要使用它的accept方法来监听这个端口的入站连接。accept方法会一直阻塞,直到一个客户端尝试连接。此时accept方法将返回一个连接客户端和服务端的Socket对象,有了这个Socket,我们就可以获取它的InputStream和OutputStream来进行数据读取和写入了。