服务器程序基本生命周期
1、使用ServerSocket构造函数在一个特定端口创建新的ServerSocket
2、使用ServerSocket的accept()方法监听该端口的入站连接,accept()方法会一直阻塞到客户端尝试建立连接,ServerSocket返回连接客户端和服务端的Sokcet对象
3、根据服务类型,调用Socket的的inputStream()或者outputStream(),获得与客户端的输入输出流
4、服务器和客户端根据已经协商好的协议交互,直到需要关闭连接
5、服务器或者客户端关闭连接
6、服务器返回步骤2,等待下一次连接
// 简单的DayTime服务器
// 测试方式 Telnet localhost 13
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class DayTimeServer {
public static final int PORT = 13;
public static void main(String[] args) {
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
try (Socket connection = server.accept()) {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
// 注意,不能依赖另一端关闭连接,
connection.close();
} catch (IOException e) {
// 忽略该异常
}
}
} catch (IOException e) {
System.out.println(e);
}
}
}
多线程版本,这里需要避免为每个连接生成一个线程,因为大量的入站连接会导致产生大量线程,最终导致虚拟机因耗尽内存而崩溃
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PooledDayTimeServer {
public static final int PORT = 13;
private static class DayTimeServer implements Callable<Void> {
private Socket connection;
public DayTimeServer(Socket connection) {
this.connection = connection;
}
@Override
public Void call() throws Exception {
// TODO Auto-generated method stub
try {
Writer out = new OutputStreamWriter(connection.getOutputStream());
Date now = new Date();
out.write(now.toString() + "\r\n");
out.flush();
} catch (IOException e) {
System.err.println(e);
} finally {
try {
connection.close();
} catch (IOException e) {
}
}
return null;
}
}
public static void main(String[] args) {
ExecutorService exe = Executors.newFixedThreadPool(50);
try (ServerSocket socket = new ServerSocket(PORT)) {
Socket connection = socket.accept();
Callable<Void> task = new DayTimeServer(connection);
exe.submit(task);
} catch (IOException e) {
System.err.println(e);
}
}
}
用Socket写入数据
以echo协议为例,写入数据和读取类似,这里需要一个InputStream,数据的写入和读取要根据协议进行;echo协议会回显收到的每个字节,不关心该自己采用了何种编码;echo没有规定锁步行为,客户端在发送一个请求后,在发送更多数据前会等待完整的服务器响应;echo协议要求客户端关闭socket,即客户端可能保持无限期连接,这里需要使用多线程的异步操作
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class EchoServer {
public static final int PORT = 7;
public static void main(String[] args) {
ServerSocketChannel serverChannel;
Selector selector;
try {
serverChannel = ServerSocketChannel.open();
ServerSocket socket = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(PORT);
socket.bind(address);
serverChannel.configureBlocking(false);
selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
System.err.println(e);
return;
}
while (true) {
try {
selector.select();
} catch (IOException e) {
System.err.println(e);
break;
}
}
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = readyKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
System.out.println("Accepted from " + client);
client.configureBlocking(false);
SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
ByteBuffer buffer = ByteBuffer.allocate(100);
clientKey.attach(buffer);
}
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
client.read(output);
}
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
output.flip();
client.write(output);
output.compact();
}
} catch (IOException e) {
key.cancel();
try {
key.channel().close();
} catch (IOException ex) {
}
}
}
}
}
关闭服务器Socket
ServerSocket在使用完后需要关闭,需要注意的是,关闭ServerSocket会释放本机的一个端口,同时还会中断ServerSockt已经接受的目前处于打开状态的所有Socket;ServerSocket实现了AutoCloseable接口;对于用无参ServerSocket构造器创建且没有绑定到任何端口的ServerSocket,并不认为其是关闭的,此时调用isClosed()返回false,使用isBound()会判断给ServerSocket是否曾经绑定过某个端口,即使该ServerSocket已经关闭,因此判断一个ServerSocket是否打开的正确方法为
public static boolean isOpen(ServerSocket ss) {
return ss.isBound && !ss.isClosed();
}
日志,服务器一般会记录两种日志
审计日志,对应与服务器建立的每个连接都会分别包含一个记录
错误日志,服务器运行期间发生的异常,注意错误日志不包含客户端的错误
Java中日志一般分为7个等级,其中错误日志一般使用SERVERE和WARNING,审计日志一般使用INFO,其他级别一般用于调试,不再生产环境中使用;日志中的每个记录,一般会包含时间戳、客户端地址、特定信息;Java会自动填入消息对应的代码位置
Level.SERVERE(最高等级)
Level.WARNING
Level.INFO
Level.CONFIG
Level.FINE
Level.FINER
Level.FINEST(最低等级)
构造服务器Socket,port 若为 0,则由系统选择可用端口,即匿名端口
public ServerSocket(int port) throws BindException, IOException 仅指定端口,则在所有网络接口或者IP地址上监听,同时设置入站连接请求长度为系统最大值
public ServerSocket(int port, int queueLength) throws BindException, IOException,
public ServerSocket(int port, int queueLength, InetAddress bindAddress) throws IOException 在指定端口、指定队列长度、指定接口上监听
public ServerSocket() throws IOException 不指定信息,Serversocket不接受任何连接,需要使用 bind() 进行绑定InetSocketAddress,若bind参数为null,则等价于 port = 0;
获取SServerSocket的相关信息
getLocalHost(),返回本机使用的地址,若有多个地址,则返回任意一个,若未绑定则返回 null
getLocalPort,返回监听的端口,若未绑定则返回 -1
Socket选项,指定ServerSocket所依赖的原生Socket是如何收发数据的
SO_TIMEOUT,从调用accept()开始计时,超时则抛出SocetTimeoutException0,必须为正整数,若为0则不设置超时,一般不设置;
public void setSoTimeout(int timeout) throws SocketException;
public int getSoTimeout() throws SocketException
SO_REUSEADDR,设置端口是否可以重用,该默认值依赖于系统,
public void setReuseAddress(boolean on) throws SocketException;
public boolean getResuseAddress() throws SocketException;
SO_RCVBUF,设置服务器Socket接收客户端Socket的接收缓冲区大小,注意该值依赖于系统,可以再绑定ServerSocket之前或者之后设置,但若想设置大于64KB的接收缓冲区,则必须在未绑定之前设置;单位B
public void setReceiveBufferSize(int size) throws SocketException
public int getReceiveBufferSize() throws SocketException
ServerSocket ss = new ServerSoket();
int receiveBufferSize = ss.getReceiveBufferSize();
if (receiveBufferSize < 128 * 1204) {
ss.setReceiveBufferSize(128 * 1024);
}
//绑定InetSocketAddress
服务类型
TCP 四种通用业务类型
1、低成本
2、高可靠性
3、最大吞吐量
4、最小延迟
通过使用 public void setPerformancePreferences(int connecrtionTime, int latency, int bandWidth) 设置连接时间、延迟、带宽的相对优先级,注意该方法设置的参数仅用作参考,具体JVM如何理解依赖于具体实现