1、多线程“服务端-客户端”
TCP客户端使用Socket来连接服务器和与服务器通信。以下为在主线程中将用户输入发送给服务端,在创建的线程中将服务端发回的数据输出来:
import java.net.*;
import java.io.*;
class ClientThread implements Runnable
{
private Socket s;
private BufferedReader br;
public ClientThread(Socket s)throws IOException
{
this.s = s;
br = new BufferedReader(new InputStreamReader(s.getInputStream()));//将Socket对应的输入流包装成BufferedReader
}
public void run()
{
try
{
String content = null;
while((content = br.readLine()) != null) //不断从服务器读取一行数据,直到对方关闭Socket
{
System.out.println("来自服务器的数据:" + content);
}
}
catch(Exception e)
{
//连接出错或当前Socket被关闭
e.printStackTrace();
}
finally
{
try
{
br.close(); //关闭输入流
s.close(); //关闭Socket
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
}
public class Client
{
public static void main(String[] args)
throws Exception
{
Socket socket = new Socket("127.0.0.1" , 30000); //连接本机IP和30000端口
//如果要设置连接超时的话应该使用无参的构造器来构造Socket,然后使用connect来设置连接超时:
//Socket socket = new Socket();
//socket.connect(new InetSocketAddress(InetAddress.getLocalHost(), 30000), 5000);
new Thread(new ClientThread(socket)).start(); //启动线程来从服务器接收数据
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));//将标准输入流包装成BufferedReader
PrintStream ps = new PrintStream(socket.getOutputStream());// 将Socket对应的输出流包装成PrintStream
//不断的从键盘读取一行数据后向服务器发送
String str = null;
while((str = br.readLine()) != null)
{
if(str.equals("quit"))
break;
else
ps.println(str);
}
br.close();
ps.close(); //关闭Socket输出流后Socket也会被关闭,可以调用shutdownInput()/shutdownOutput()来只关闭输入/输出流。
}
}
TCP服务端使用使用ServerSocket对象绑定IP地址和端口并监听客户端的连接,使用Socket对象来与客户端通信,一个连接对应一个Socket。以下为多线程的服务端,当收到用户数据后再发回给用户:
import java.net.*;
import java.io.*;
class ServerThread implements Runnable
{
Socket s = null;
BufferedReader br = null;
public ServerThread(Socket s)throws IOException
{
this.s = s;
br = new BufferedReader(new InputStreamReader(s.getInputStream()));//将Socket对应的输入流包装成BufferedReader
}
public void run()
{
try
{
//s.setSoTimeout(5000); //设置读写超时
String str = null;
//不断从客户端读取一行数据并向客户端发送,直到对方关闭Socket
while((str = br.readLine()) != null)
{
PrintStream ps = new PrintStream(s.getOutputStream());// 将Socket对应的输出流包装成PrintStream
ps.println(str);
}
System.out.println("对方关闭连接");
}
catch(Exception e)
{
//连接出错或当前Socket被关闭
e.printStackTrace();
}
finally
{
try
{
br.close(); //关闭输出流
s.close(); //关闭Socket
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
}
public class Server
{
public static void main(String[] args)
throws Exception
{
ServerSocket ss = null;
try
{
ss = new ServerSocket(30000); //绑定本地IP和端口30000
//ServerSocket ss = new ServerSocket(30000, 100); //指定连接队列长度
//ServerSocket ss = new ServerSocket(30000, 100, InetAddress.getLocalHost()); //绑定指定IP地址
// 采用循环不断接受来自客户端的连接请求
while (true)
{
Socket s = ss.accept(); //监听和接收连接
System.out.println("接收到一个连接:" + s.getInetAddress().getHostAddress() + "," + s.getPort());
new Thread(new ServerThread(s)).start();
}
}
catch(Exception e)
{
throw e;
}
finally
{
ss.close();
}
}
}
2、多路复用器Selector + 非阻塞Socket IO
TCP服务端使用ServerSocketChannel对象(通过ServerSocketChannel的类方法open()创建)绑定IP地址和端口并监听客户端的连接,当连接建立后使用SocketChannel对象来与客户端通信,一个连接对应一个SocketChannel。ServerSocketChannel和SocketChannel都是SelectableChannel的子(孙)类,一个SelectableChannel可以注册到多路复用器Selector上。SelectableChannel中常用给的方法有:
validOps():获得该Channel支持的IO操作(ServerSocketChannel只支持OP_ACCEPT(16), SocketChannel支持OP_CONNECT(8)、OP_READ(1)、OP_WRITE(4),比如validOps()返回5的话可以知道其只支持读(1)和写(4))。
isRegistered():判断该Channel是否已经注册在一个selector上。
keyFor(Selector):获得该Channel和指定Selector的注册关系。
SelectionKey类表示SelectableChannel和Selector的注册关系,一个注册了的Channel对应一个SelectionKey对象。SocketChannel实现了ByteChannel、ScatteringByteChannel(分散通道,可以读取数据到多个缓冲区(缓冲区数组)中)、GatheringByteChannel(聚集通道,可以从多个缓冲区(缓冲区数组)中获得数据)接口,所以可以直接读写ByteBuffer对象。
以下为服务器端示例,它接收客户端的数据后向所有连接的客户端发送该数据:
import java.net.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
public class NServer
{
// 用于检测所有Channel(ServerSocketChannel、SocketChannel)状态(有连接到来、有数据可读)的Selector对象
private Selector selector = null;
// 定义实现编码、解码的字符集对象
private Charset charset = Charset.forName("UTF-8");
public void init()throws IOException
{
selector = Selector.open(); //创建多路复用器
ServerSocketChannel server = ServerSocketChannel.open(); //创建ServerSocketChannel实例
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);
server.bind(isa); // 将ServerSocket绑定到指定IP地址
server.configureBlocking(false); // 设置ServerSocket以非阻塞方式工作
server.register(selector, SelectionKey.OP_ACCEPT); // 将ServerSocketChannel注册到指定Selector对象,一个Channel对应一个SelectionKey对象
while (selector.select() > 0) //select()/select(timeout)/selectNow()监控selector上是否有需要处理的Channel,它们称为被选择的SelectionKey集合
{
for (SelectionKey sk : selector.selectedKeys()) // 依次处理selector上被选择的SelectionKey集合(selectedKeys()可以获得selector上所有需要处理的Channel对应的SelectionKey)
{
selector.selectedKeys().remove(sk); // 从selector上的已选择SelectionKey集中删除正在处理的SelectionKey
if (sk.isAcceptable()) //有连接请求
{
SocketChannel sc = server.accept(); // 调用accept方法接受连接,产生服务器端的SocketChannel
sc.configureBlocking(false); // 设置采用非阻塞模式
sc.register(selector, SelectionKey.OP_READ); // 将该SocketChannel注册到selector
sk.interestOps(SelectionKey.OP_ACCEPT); // 将sk对应的Channel设置成准备接受下一次连接请求
}
if (sk.isReadable()) // Channel中有数据可读
{
SocketChannel sc = (SocketChannel)sk.channel();// 获取该SelectionKey对应的Channel
//准备读取该Channel中的数据
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
int readBytes = 0;
try
{
while((readBytes = sc.read(buff)) > 0)
{
buff.flip();
content += charset.decode(buff);
}
if(readBytes >= 0)
{
System.out.println("读取到的数据:" + content);
sk.interestOps(SelectionKey.OP_READ);// 将sk对应的Channel设置成准备下一次读取
}
else if(readBytes < 0) //对方关闭Socket
{
sk.cancel(); // 从Selector中删除指定的SelectionKey
if (sk.channel() != null)
{
sk.channel().close(); //关闭该Channel
}
}
}
catch (IOException ex)// 如果捕捉到该sk对应的Channel出现了异常,即表明该Channel对应的Socket出现了问题,所以从Selector中取消sk的注册
{
sk.cancel(); // 从Selector中删除指定的SelectionKey
if (sk.channel() != null)
{
sk.channel().close(); //关闭该Channel
}
}
// 如果收到的数据content的长度大于0,则将数据分发给所有连接
if (content.length() > 0)
{
// 遍历该selector里注册的所有SelectionKey,keys()用来获得所有注册在该多路复用器上的Channel对应的SelectionKey
// 无需使用集合来保存所有客户端的SocketChannel,因为它们都被注册到了selector上,可以通过keys()获得
for (SelectionKey key : selector.keys())
{
Channel targetChannel = key.channel(); // 获取该key对应的Channel
if (targetChannel instanceof SocketChannel) // 如果该channel是SocketChannel对象
{
// 将content数据写入该Channel中,即发送数据到该Socket
SocketChannel dest = (SocketChannel)targetChannel;
dest.write(charset.encode(content));
}
}
}
}
/*if(sk.isWritable()) // Channel可写
{
sk.interestOps(SelectionKey.OP_WRITE);
}*/
}
}
}
public static void main(String[] args)
throws IOException
{
new NServer().init();
}
}
客户端使用SocketChannel对象与服务器通信,创建SocketChannel对象也是通过SocketChannel的类方法open():
Selector selector = Selector.open(); //创建多路复用器
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);
SocketChannel sc = SocketChannel.open(isa); //创建SocketChannel对象并连接到指定服务器
sc.configureBlocking(false); // 设置该sc以非阻塞方式工作
sc.register(selector, SelectionKey.OP_READ); // 将SocketChannel对象注册到指定Selector
......
3、AIO
AIO即异步IO,Java中的AIO主要通过AsynchronousServerSocketChannel、AsynchronousSocketChannel对象和CompletionHandler接口、ExecutorService线程池来实现。
服务端通过AsynchronousServerSocketChannel绑定本机IP和端口后执行其accept()方法来异步接受连接,accept()的第二个参数是一个实现了CompletionHandler
客户端通过AsynchronousSocketChannel来与服务端进行通信。
4、UDP
Java中使用DatagramSocket来接收和发送数据报,DatagramPacket代表数据报,MulticastSocket可以将数据报以广播方式发送给多个客户端。
5、代理服务器
使用代理服务器可以突破自身IP限制来访问受限的或内部站点,使用它还可以对外隐藏自身IP地址和提高访问速度(代理服务器具有缓冲功能)。
代理服务器的类型主要有HTTP代理和SOCKS代理,HTTP用来代理客户机的HTTP访问,它的端口一般为80、8080、3128等。SOCKS不关心使用何种协议,只是简单地传递数据包,SOCKS4代理只支持TCP协议,SOCKS5代理则既支持TCP协议又支持UDP协议,还支持各种身份验证机制、服务器端域名解析等。
设置使用代理服务器主要有三种方法:
①、在调用URL的openConnection获得该URL的连接对象或者在创建Socket的时候可以传入一个Proxy对象,通过该设置该Proxy对象来指定使用的代理服务器。
②、也可以创建一个ProxySelector的实现对象,然后调用ProxySelector的类方法setDefault()来注册该代理服务器,这样我们在连接URL或创建Socket的时候默认就会使用ProxySelector对象设置的代理服务器。
③、还可以通过设置Java系统属性(使用System的setProperties())来设置代理服务器,因为Java系统默认会使用一个ProxySelector实现对象来作为代理服务器,在该ProxySelector对象设置中会根据系统属性来选择代理服务器。
上面所说的代理服务器实际上是正向代理服务器,还有一种反向代理服务器。反向代理是指客户端发送请求到“中间”服务器上,“中间”服务器再根据“负载均衡”算法将请求转发到指定的实际服务器上,实际服务器也会将响应数据返回给“中间”服务器,再由“中间”服务器将响应数据返回给客户端,这里的“中间”服务器就被称为反向代理服务器。正向代理是由客户端进行设置的,而反向代理由后端进行配置。