Java网络编程(2):TCP和UDP

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接口的对象(泛型V指定了IO操作返回的对象的类型,对于accept操作V应该为AsynchronousSocketChannel类型,即accept成功后与客户端进行通信的SocketChannel,对于读写操作V应该为Integer类型,即读写操作的实际执行量),执行accept()方法后会立即返回。当有客户连接成功后会通过传给accept()的CompletionHandler对象来得到通知: IO事件通知会通过创建AsynchronousServerSocketChannel时传入的线程池中的线程来执行,具体为执行CompletionHandler对象的两个方法completed(IO操作完成执行)和failed(IO操作失败执行),completed方法的第一个参数为IO操作返回的对象(对于读/写操作为读/写执行的字节量Integer类型。对于accept为连接成功后创建的与客户端对应的AsynchronousSocketChannel对象,调用该对象的read()、write()与客户端进行异步读写操作,read/write同样接收一个CompletionHandler对象参数,当IO操作完成以后会回调执行该对象的completed或failed方法进行通知),第二个参数为发起IO操作时传入的附加参数,failed方法的第一个参数为IO操作失败引发的异常或错误,第二个参数为发起IO操作时传入的附加参数。

  客户端通过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对象设置中会根据系统属性来选择代理服务器。

  上面所说的代理服务器实际上是正向代理服务器,还有一种反向代理服务器。反向代理是指客户端发送请求到“中间”服务器上,“中间”服务器再根据“负载均衡”算法将请求转发到指定的实际服务器上,实际服务器也会将响应数据返回给“中间”服务器,再由“中间”服务器将响应数据返回给客户端,这里的“中间”服务器就被称为反向代理服务器。正向代理是由客户端进行设置的,而反向代理由后端进行配置。

  

你可能感兴趣的:(Java,SE)