《Thinking in Enterprise Java》中第一章描述了用Socket和Channel的网络编程,核心即为Socket和Channel,本文简单讲述Socket的应用。
Socket可以认为是两个互联机器终端应用软件的抽象,即对于一个网络连接,两端都有一个Socket,应用可以通过套接字进行交互通信。
在Java中,创建Socket连接另一台机器,可以从Socket中获取InputStream和OutputStream,将其作为输入输出流,使应用程序与操作本地文件IO类似。存在2个基于流的Socket类:ServerSocket和Socket。
- ServerSocket用于服务器端,监听客户端连接
- Socket用于客户端与服务端交互
- 服务段accept()方法处于阻塞状态,直到有客户端连接,创建一个服务端Socket,与客户端交互
另外,当创建ServerSocket时,只需要提供一个端口号,IP信息为本机默认信息;创建Socket时,必须提供IP和端口号;由ServerSocket.accept( )创建的不需要,其已包含所有信息。
1. 简单客户端和服务端
服务器端:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket; public class JabberServer { public static final int PORT = 8080;
public static void main(String[] args) throws IOException{ ServerSocket server = new ServerSocket(PORT);
System.out.println("开始: " + server);
try {
Socket socket = server.accept();
System.out.println("Connection socket: " + socket);
try {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//Output is automatically flushed by PrintWrite
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
while(true) {
String str = in.readLine();
if("END".equals(str))
break;
System.out.println("Echoing: " + str);
out.println(str);
}
} finally {
System.out.println("CLosing....");
socket.close();
}
} finally {
server.close();
}
}
}
输出:
开始: ServerSocket[addr=0.0.0.0/0.0.0.0,localport=8080]
大致步骤:
- 创建ServerSocket,绑定端口8080
- 调accept()方法监听连接,并返回套接字Socket
- 获取输入流,并通过InputStreamReader转为字符,缓存在BufferdReader中
- 获取输出流,通过OutputStreamWriter将BufferedWriter中的字符转换为字节,并通过PrintWriter格式化输出,同时自动flush
- 根据输入流读取的字符,如果是END则结束会话
- 关闭套接字和ServerSocket
客户端:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket; /**
* 根据服务器ip和端口/服务器地址等,创建Socket
* Socket可以获取输入和输出流,默认是使用AbstractPlainSocketImpl类中的SocketInputStream和SocketOutputStream
*
*/
public class JabberClient { public static void main(String[] args) throws Exception{ //服务器端信息,address和8080;后台连接服务器,还会绑定客户端
InetAddress address = InetAddress.getByName(null);
System.out.println("address = " + address);
Socket socket = new Socket(address, 8080); try {
System.out.println("Socket = " +socket);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true); for(int i = 0; i < 10; i++) {
out.println("hello " + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
} finally {
System.out.println("Closing socket...");
socket.close();
}
}
}
大致步骤与服务端相似,不同之处在于创建客户端套接字,需要指定服务端地址和端口号。
服务器端输出:
Connection socket: Socket[addr=/127.0.0.1,port=35702,localport=8080]
Echoing: hello 0
Echoing: hello 1
Echoing: hello 2
Echoing: hello 3
Echoing: hello 4
Echoing: hello 5
Echoing: hello 6
Echoing: hello 7
Echoing: hello 8
Echoing: hello 9
CLosing....
客户端输出:
address = localhost/127.0.0.1
Socket = Socket[addr=localhost/127.0.0.1,port=8080,localport=35702]
hello 0
hello 1
hello 2
hello 3
hello 4
hello 5
hello 6
hello 7
hello 8
hello 9
Closing socket...
2. 源码分析
无论是创建ServerSocket还是Socket,都需要与底层实现SocketImpl关联,以实现具体网络交互的逻辑。
(1)服务端创建ServerSocket
ServerSocket server = new ServerSocket(PORT);
具体构造过程如下,构造参数为服务端监听端口:
/**
* Creates a server socket, bound to the specified port. A port number
* of {@code 0} means that the port number is automatically
* allocated, typically from an ephemeral port range. This port
* number can then be retrieved by calling {@link #getLocalPort getLocalPort}.
* <p>
* The maximum queue length for incoming connection indications (a
* request to connect) is set to {@code 50}. If a connection
* indication arrives when the queue is full, the connection is refused.
* <p>
* If the application has specified a server socket factory, that
* factory's {@code createSocketImpl} method is called to create
* the actual socket implementation. Otherwise a "plain" socket is created.
* <p>
* If there is a security manager,
* its {@code checkListen} method is called
* with the {@code port} argument
* as its argument to ensure the operation is allowed.
* This could result in a SecurityException.
*
*
* @param port the port number, or {@code 0} to use a port
* number that is automatically allocated.
*
* @exception IOException if an I/O error occurs when opening the socket.
* @exception SecurityException
* if a security manager exists and its {@code checkListen}
* method doesn't allow the operation.
* @exception IllegalArgumentException if the port parameter is outside
* the specified range of valid port values, which is between
* 0 and 65535, inclusive.
*
* @see java.net.SocketImpl
* @see java.net.SocketImplFactory#createSocketImpl()
* @see java.net.ServerSocket#setSocketFactory(java.net.SocketImplFactory)
* @see SecurityManager#checkListen
*/
public ServerSocket(int port) throws IOException {
this(port, 50, null);
} /**
* Create a server with the specified port, listen backlog, and
* local IP address to bind to. The <i>bindAddr</i> argument
* can be used on a multi-homed host for a ServerSocket that
* will only accept connect requests to one of its addresses.
* If <i>bindAddr</i> is null, it will default accepting
* connections on any/all local addresses.
* The port must be between 0 and 65535, inclusive.
* A port number of {@code 0} means that the port number is
* automatically allocated, typically from an ephemeral port range.
* This port number can then be retrieved by calling
* {@link #getLocalPort getLocalPort}.
*
* <P>If there is a security manager, this method
* calls its {@code checkListen} method
* with the {@code port} argument
* as its argument to ensure the operation is allowed.
* This could result in a SecurityException.
*
* The {@code backlog} argument is the requested maximum number of
* pending connections on the socket. Its exact semantics are implementation
* specific. In particular, an implementation may impose a maximum length
* or may choose to ignore the parameter altogther. The value provided
* should be greater than {@code 0}. If it is less than or equal to
* {@code 0}, then an implementation specific default will be used.
* <P>
* @param port the port number, or {@code 0} to use a port
* number that is automatically allocated.
* @param backlog requested maximum length of the queue of incoming
* connections.
* @param bindAddr the local InetAddress the server will bind to
*
* @throws SecurityException if a security manager exists and
* its {@code checkListen} method doesn't allow the operation.
*
* @throws IOException if an I/O error occurs when opening the socket.
* @exception IllegalArgumentException if the port parameter is outside
* the specified range of valid port values, which is between
* 0 and 65535, inclusive.
*
* @see SocketOptions
* @see SocketImpl
* @see SecurityManager#checkListen
* @since JDK1.1
*/
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;
}
}
实际调用的构造方法为ServerSocket(int port, int backlog, InetAddress bindAddr),port表示端口号,backlog表示服务端请求连接队列最大数(默认为50),bindAddr表示服务器要绑定的本地地址(默认为null)。
- setImpl(),设置系统默认类型SocketImpl,其是服务端和客户端套接字创建、连接、交互等操作的核心
- bind(new InetSocketAddress(bindAddr, port), backlog),绑定对应的地址和端口,并设置最大连接数(超过连接数,服务器拒绝连接)
(2)服务端accept
Socket socket = server.accept();
服务端会阻塞等待客户端连接,直到有客户端连接,并创建一个服务端Socket,与客户端交互。
/**
* Listens for a connection to be made to this socket and accepts
* it. The method blocks until a connection is made.
*
* <p>A new Socket {@code s} is created and, if there
* is a security manager,
* the security manager's {@code checkAccept} method is called
* with {@code s.getInetAddress().getHostAddress()} and
* {@code s.getPort()}
* as its arguments to ensure the operation is allowed.
* This could result in a SecurityException.
*
* @exception IOException if an I/O error occurs when waiting for a
* connection.
* @exception SecurityException if a security manager exists and its
* {@code checkAccept} method doesn't allow the operation.
* @exception SocketTimeoutException if a timeout was previously set with setSoTimeout and
* the timeout has been reached.
* @exception java.nio.channels.IllegalBlockingModeException
* if this socket has an associated channel, the channel is in
* non-blocking mode, and there is no connection ready to be
* accepted
*
* @return the new Socket
* @see SecurityManager#checkAccept
* @revised 1.4
* @spec JSR-51
*/
public Socket accept() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isBound())
throw new SocketException("Socket is not bound yet");
Socket s = new Socket((SocketImpl) null);
implAccept(s);
return s;
}
① 指定SocketImpl,创建一个非连接的Socket构造方法,如下:
/**
* Creates an unconnected Socket with a user-specified
* SocketImpl.
* <P>
* @param impl an instance of a <B>SocketImpl</B>
* the subclass wishes to use on the Socket.
*
* @exception SocketException if there is an error in the underlying protocol,
* such as a TCP error.
* @since JDK1.1
*/
protected Socket(SocketImpl impl) throws SocketException {
this.impl = impl;
if (impl != null) {
checkOldImpl();
this.impl.setSocket(this);
}
}
② implAccept(s)方法
上一步创建Socket的参数SocketImpl为null,该方法为Socket创建具体的SocketImpl,绑定地址和文件描述符,具体可见源码。
(3)客户端创建套接字
Socket socket = new Socket(address, 8080);
具体创建过程如下,构造参数为InetAddress和port:
/**
* Creates a stream socket and connects it to the specified port
* number at the specified IP address.
* <p>
* If the application has specified a socket factory, that factory's
* {@code createSocketImpl} method is called to create the
* actual socket implementation. Otherwise a "plain" socket is created.
* <p>
* If there is a security manager, its
* {@code checkConnect} method is called
* with the host address and {@code port}
* as its arguments. This could result in a SecurityException.
*
* @param address the IP address.
* @param port the port number.
* @exception IOException if an I/O error occurs when creating the socket.
* @exception SecurityException if a security manager exists and its
* {@code checkConnect} method doesn't allow the operation.
* @exception IllegalArgumentException if the port parameter is outside
* the specified range of valid port values, which is between
* 0 and 65535, inclusive.
* @exception NullPointerException if {@code address} is null.
* @see java.net.Socket#setSocketImplFactory(java.net.SocketImplFactory)
* @see java.net.SocketImpl
* @see java.net.SocketImplFactory#createSocketImpl()
* @see SecurityManager#checkConnect
*/
public Socket(InetAddress address, int port) throws IOException {
this(address != null ? new InetSocketAddress(address, port) : null,
(SocketAddress) null, true);
} 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;
}
}
- 首先,setImpl(),与服务端相似,设置系统默认类型SocketImpl,其是服务端和客户端套接字创建、连接、交互等操作的核心
- 其次,createImpl(stream),根据stream布尔值创建socket实现,true时创建基于流的socket(或者面向连接),false时创建无连接UDP套接字
- 最后,connect(address),连接服务器,连接一直处于阻塞状态,直到连接成功,或者超时或报错等
(4)SocketImpl类
SocketImpl类是服务器和客户端连接的核心,源码如下,包含Socket、ServerSocket、文件描述符、IP地址、端口号和套接字连接的本地端口号:
/**
* The abstract class {@code SocketImpl} is a common superclass
* of all classes that actually implement sockets. It is used to
* create both client and server sockets.
* <p>
* A "plain" socket implements these methods exactly as
* described, without attempting to go through a firewall or proxy.
*
* @author unascribed
* @since JDK1.0
*/
public abstract class SocketImpl implements SocketOptions {
/**
* The actual Socket object.
*/
Socket socket = null;
ServerSocket serverSocket = null; /**
* The file descriptor object for this socket.
*/
protected FileDescriptor fd; /**
* The IP address of the remote end of this socket.
*/
protected InetAddress address; /**
* The port number on the remote host to which this socket is connected.
*/
protected int port; /**
* The local port number to which this socket is connected.
*/
protected int localport;
类结构如下:
(5)套接字输入输出流
SocketImpl默认子类是AbstractPlainSocketImpl,大部分套接字的创建、连接等操作都通过该类进行。套接字通过获取输入输出流,使应用可以像操作本地I/O流一样,操作网络数据。
Socket获取输入输出流的方法是getInputStream()和getOutputStream(),底层调AbstractPlainSocketImpl的方法获取,实际流对象为SocketInputStream和SocketOutputStream,具体细节此处不阐述。
socket.getInputStream()//获取输入流
socket.getOutputStream()//获取输出流
AbstractPlainSocketImpl类中包含2个套接字输入输出流属性,如下:
private SocketInputStream socketInputStream = null;
private SocketOutputStream socketOutputStream = null;
他们分别继承自FileInputStream和FileOutputStream,以表示输入输出源。并且他们都不是public类型的,一般不会直接使用。
class SocketInputStream extends FileInputStream
class SocketOutputStream extends FileOutputStream
3. 总结
1. 网络连接的核心是套接字Socket,服务端ServerSocket监听连接,创建套接字;客户端创建套接字,绑定服务端ip和端口
2. SocketImpl类和子类AbstractPlainSocketImpl是服务端和客户端套接字创建、连接、交互的核心
3. 通过套接字获取输入输出流,应用程序可以与本地I/O流操作一样