java TCP/IP Socket编程-----进阶--多线程-----笔记8

概述:

如何充分利用cpu多核资源,以前是开启多进程,现在更加轻量级的多线程来实现,实现多线程,常用就是Thread类和实现Runnable接口,通常采用后者,多线程主要是重写run()方法所以需要开启线程的事情放入到run()方法内,而服务器就是处理客户端请求,所以我们要把处理客户端的工作搬到run方法内

1.服务端的线程类 EchoProtocol.java

package com.tcp.ip.chapter4;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;

public class EchoProtocol implements Runnable{

	//缓存
	private static final int BUFSIZE = 32;
	//客户端套接字
	private Socket clntSock;
	//服务器日志
	private Logger logger;
	public EchoProtocol(Socket clntSock, Logger logger){
		this.clntSock = clntSock;
		this.logger = logger;
	}
	
	public static void handleEchoClient(Socket clntSock, Logger logger){
		try {
			//从socket 获取输入流和输出流
			InputStream in  = clntSock.getInputStream();
			OutputStream out = clntSock.getOutputStream();
			//接收信息的大小
			int recvMsgSize; 
			// 来客户端的字节树
			int totalBytesEchoed = 0; 
			byte[] echoBuffer = new byte[BUFSIZE];
			//接受数据直到为-1
			while ((recvMsgSize = in.read(echoBuffer)) != -1) {
				out.write(echoBuffer,0,recvMsgSize);
				totalBytesEchoed += recvMsgSize;
			}
			logger.info("client" + clntSock.getRemoteSocketAddress() + ", echoed "
					+ totalBytesEchoed + " bytes.");
			
		} catch (IOException ex) {
			logger.log(Level.WARNING, "Exception in echo protocol" , ex);
		}finally {
			try {
				clntSock.close();
			} catch (IOException e) {
				
			}
		}
	}
	public void run() {
		handleEchoClient(clntSock, logger);
	}

}

总结:

  • 1.打印异常日志的时候,一般会用logger.log() ,而不会直接打印堆栈信息,成本太高
  • 2.finally内close方法,有可能调用关不掉,一般会增加一个try catch进行异常捕捉

2.一客户一线程  TCPEchoServerThread.java

package com.tcp.ip.chapter4;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Logger;

public class TCPEchoServerThread {

	public static void main(String[] args) throws IOException{
		
		if(args.length != 1) {
			throw new IllegalArgumentException("Parameter(s) : ");
		}
		int echoServPort = Integer.parseInt(args[0]);
		
		//创建一个socket接受客户端请求连接
		ServerSocket servSock = new ServerSocket(echoServPort);
		Logger logger = Logger.getLogger("practical");
		while (true) {
			Socket clntSock = servSock.accept();
			
			Thread thread = new Thread(new EchoProtocol(clntSock, logger));
			thread.start();
			logger.info("创建和启动的线程  " + thread.getName());
		}
	}
}

总结:直接创建一个线程接受客户端请求,当大量客户端请求会创建很多请求,使得cpu调用变很慢,有可能卡死了,这是我们考虑使用线程池,限制创建线程的数量

3.线程池类 TCPEchoServerPool.java

package com.tcp.ip.chapter4;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TCPEchoServerPool {

	public static void main(String[] args) throws IOException{
		if (args.length != 2) {
			throw new IllegalArgumentException("Parameter(s) :  ");
		}
		
		int echoServPort = Integer.parseInt(args[0]); 
		int threadPoolSize = Integer.parseInt(args[1]);
		//创建服务socket接受客户端连接
		final ServerSocket servSock = new ServerSocket(echoServPort);
		
		final Logger logger = Logger.getLogger("practical");
		for (int i = 0; i < threadPoolSize; i++) {
			Thread thread = new Thread() {
				public void run(){
					while (true) {
						try {
							Socket clntSock = servSock.accept();
							EchoProtocol.handleEchoClient(clntSock, logger);
						} catch (IOException ex){
							logger.log(Level.WARNING, "Client accept fialed", ex);
						}
					}
				}
			};
			thread.start();
			logger.info("创建和启动线程 = " + thread.getName());
		}
		
		
	}
}

总结:这里我们可以设置线程的总数量,防止创建过多的线程,但是如果线程死了,那么没有补给,就是像打仗的时候,前沿部队不断增员,这是需要一个调度者,知道什么时候该增员的了。

4.调度者 TCPEchoServerExecutor.java

package com.tcp.ip.chapter4;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.logging.Logger;

public class TCPchoServerExecutor {

	public static void main(String[] args) throws IOException {
		
		if(args.length != 1) {
			throw new IllegalArgumentException("Parameter(s) : ");
		}
		int echoServPort = Integer.parseInt(args[0]);
		//创建缓存的线程池
		Executor service = Executors.newCachedThreadPool();
		//创建一个服务套接字接受客户请求
		ServerSocket servSock = new ServerSocket(echoServPort);
		Logger logger = Logger.getLogger("practical");
		while (true) {
			Socket clntSock = servSock.accept();
			service.execute(new EchoProtocol(clntSock, logger));
			
		}
	}
}

总结:由系统判断多少线程是最优的,Executors.newCachedThreadPool(),当前还有其它调用方法,例如固定大小的线程Executors.newFixedThreadPool(线程大小);单个线程

Executors.newSingleThreadExecutor();

5.阻塞和超时

Socket的I/O调用可能会因为多种原因而阻塞。数据输入方法read()和receive()在没有数据可读时会阻塞。TCP套接字的write()方法在没有足够的空间缓存传输的数据时可能阻塞。 ServerSocket的accept()方法和Socket的构造函数都会阻塞等待

5.1.accept(),read()和receive():

对于这些方法,我们可以使用Socket类、ServerSocket类和DatagramSocket类的setSoTimeout()方法,设置其阻塞的最长时间(以毫秒为单位)。如果在指定时间内这些方法没有返回,则将抛出一个InterruptedIOException异常。对于Socket实例,在调用read()
方法前,我们还可以使用该套接字的InputStream的available()方法来检测是否有可读的数据。

5.2.连接和写入数据

Socket类的构造函数会尝试根据参数中指定的主机和端口来建立连接,并阻塞等待,直到连接成功建立或发生了系统定义的超时。不幸的是,系统定义的超时时间很长,而Java又没有提供任何缩短它的方法。要改变这种情况,可以使用Socket类的无参数构造函数,它返回的是一个没有建立连接的Socket实例。需要建立连接时,调用该实例的connect()方法,并指定一个远程终端和超时时间(毫秒)

write()方法调用也会阻塞等待,直到最后一个字节成功写入到了TCP实现的本地缓存中。如果可用的缓存空间比要写入的数据小,在write()方法调用返回前,必须把一些数据成功传输到连接的另一端

5.3限制每个客户端的时间:

package com.tcp.ip.chapter4;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TimeLimitEchoProtocol implements Runnable {

	private static final int BUFSIZE = 32;
	private static final String TIMELIMIT = "10000";
	private static final String TIMELIMITPROP = "Timelimit";
	
	private static int timelimit;
	
	private Socket clntSock;

	private Logger logger;
	
	public TimeLimitEchoProtocol(Socket clntSock, Logger logger){
		
		this.clntSock = clntSock;
		this.logger = logger;
		timelimit = Integer.parseInt(System.getProperty(TIMELIMITPROP, TIMELIMIT));
	}
	
	public static void handleEchoClient(Socket clntSock, Logger logger){
		try {
			//得到输入流和输出流
			InputStream in = clntSock.getInputStream();
			OutputStream out = clntSock.getOutputStream();
			//接受信息的大小
			int recvMsgSize; 
			int totalBytesEchoed = 0;
			byte[] echoBuffer = new byte[BUFSIZE];
			//算出最后结束的时间
			long endTime = System.currentTimeMillis() + timelimit;
			int timeBoundMillis = timelimit;
			clntSock.setSoTimeout(timeBoundMillis);
			
			while ((timeBoundMillis > 0) && 
					((recvMsgSize = in.read(echoBuffer)) != -1)) {
				out.write(echoBuffer, 0, recvMsgSize);
				totalBytesEchoed += recvMsgSize;
				//当前剩余时间
				timeBoundMillis = (int) (endTime - System.currentTimeMillis());
				clntSock.setSoTimeout(timeBoundMillis);
				logger.info("客户端 " + clntSock.getRemoteSocketAddress() +
						", echoed " + totalBytesEchoed + " bytes.");
				
			}
		} catch (IOException ex) {
			logger.log(Level.WARNING, "Exception in echo protocol" , ex);
		}
	}
	
	public void run() {
		handleEchoClient(this.clntSock, this.logger);
		
	}

}
总结:通过系统时间来确定超时时间。

6.多播和广播

有两种类型的一对多(one-to-many)服务:广播(broadcast)和多播(multicast)。对于广播,(本地)网络中的所有主机都会接收到一份数据副本。对于多播,消息只是发送给一个多播地址(multicast address),网络只是将数据分发给那些表示想要接收发送到该多播地址的数据的主机。总的来说,只要UDP套接字允许广播或多播。

广播UDP数据报文与单播数据报文相似,唯一的区别是其使用的是一个广播地址而不是一个常规的(单播)IP地址。注意,IPv6并没有明确地提供广播地址;然而,有一个特殊的全节点(all - nodes)、本地连接范围(link-local-scope)的多播地址,FFO2::1,发送给该地址的消息将多播到一个连接上的所有节点。IPv4的本地广播地址(255.255.255.255)将消息发送到在同一广播网络上的每个主机。本地广播信息决不会被路由器转发。在以太网上的一个主机可以向在同一以太网内的其他主机发送消息,但是该消息不会被路由器转发。IPv4还指定了定向广播地址,允许向指定网络中的所有主机进行广播;然而,由于互联网上的大部分路由器都不转发定向广播消息。

与广播一样,多播与单播之间的一个主要区别是地址的形式。一个多播地址指示了一组接收者。IP协议的设计者为多播分配了一定范围的地址空间,IPv4中的多播地址范围是224.0.0.0到239.255.255.255,IPv6中的多播地址是任何由FF开头的地址。除了少数系统保留的多播地址外,发送者可以向以上范围内的任何地址发送数据。Java中多播应用程序主要通过MulticastSocke实例进行通信,它是DatagramSocket的一个子类。重点需要理解的是,一个MulticastSocket实例实际上就是一个UDP套接字(DatagramSocket),其包含了一些额外的可以控制的多播特定属性。

多播发送者:

package com.tcp.ip.chapter4.cast;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;

import com.tcp.ip.chapter3.vote.VoteMsg;
import com.tcp.ip.chapter3.vote.VoteMsgCoder;
import com.tcp.ip.chapter3.vote.VoteMsgTextCoder;

public class VoteMulticastSender {

	public static final int CANDIDATEID = 475;
	
	public static void main(String args[] ) throws IOException{
		
		if((args.length < 2) || (args.length > 3)){
			throw new IllegalArgumentException("Parameter(s): []");
		}
		
		InetAddress destAddr = InetAddress.getByName(args[0]);
		
		if( !destAddr.isMulticastAddress()) {
			throw new IllegalArgumentException ("不是一个多播地址");
		}
		int destPort = Integer.parseInt(args[1]);
		int TTL = (args.length == 3) ? Integer.parseInt(args[2]) : 1;
		
		MulticastSocket sock = new MulticastSocket();
		sock.setTimeToLive(TTL);
		VoteMsgCoder coder = new VoteMsgTextCoder();
		VoteMsg vote = new VoteMsg(true , true, CANDIDATEID, 1000001L);
		byte[] msg = coder.toWire(vote);
		DatagramPacket message = new DatagramPacket(msg, msg.length, destAddr, destPort);
		System.out.println("发送 文本 请求" + msg.length + " bytes) :");
		System.out.println(vote);
		sock.send(message);
		sock.close();
	}
}
总结:主要与单播区别就是,会判断是否多播地址destAddr.isMulticastAddress() 设置传播的生命周期,sock.setTimeToLive(TTL)

多播接收者

package com.tcp.ip.chapter4.cast;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.Arrays;

import com.tcp.ip.chapter3.vote.VoteMsg;
import com.tcp.ip.chapter3.vote.VoteMsgTextCoder;

public class VoteMulticastReceiver {

	public static void main(String[] args) throws IOException {
		
		if(args.length != 2){
			throw new IllegalArgumentException("Parameter(s) :");
		}
		InetAddress address = InetAddress.getByName(args[0]);
		
		if (!address.isMulticastAddress()) {
			throw new IllegalArgumentException("没一个多播地址");
		}
		
		int port = Integer.parseInt(args[1]);
		
		MulticastSocket sock = new MulticastSocket(port);
		sock.joinGroup(address);
		
		VoteMsgTextCoder coder = new VoteMsgTextCoder();
		
		//接受datagram
		DatagramPacket packet = new DatagramPacket(new byte[VoteMsgTextCoder.MAX_WIRE_LENGTH],
				VoteMsgTextCoder.MAX_WIRE_LENGTH);
		sock.receive(packet);
		
		VoteMsg vote = coder.fromWire(Arrays.copyOfRange(packet.getData(), 0, packet.getLength()));
		System.out.println("接受文本请求( " + packet.getLength() + " bytes: ");
		System.out.println(vote);
		sock.close();
	}
	
}
总结:多一个加入组的动作,sock.joinGroup(address);

单次关闭输入流和输出流

package com.tcp.ip.chapter4.cast;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class CompressClient {

	public static final int BUFSIZE = 256;
	
	public static void main(String[] args)throws IOException{
		
		if(args.length != 3){
			throw new IllegalArgumentException("Parameters:   ");
		}
		
		String server = args[0];
		int port = Integer.parseInt(args[1]);
		String filename = args[2];
		
		FileInputStream fileIn = new FileInputStream(filename);
		
		FileOutputStream fileOut = new FileOutputStream(filename + ".gz");
		
		//创建连接
		Socket sock = new Socket(server, port);
		
		//发送未解压的数据流给服务器
		sendBytes(sock, fileIn);
		
		//接收数据
		InputStream sockIn = sock.getInputStream();
		int bytesRead;
		byte[] buffer = new byte[BUFSIZE];
		while ((bytesRead = sockIn.read(buffer)) != -1){
			fileOut.write(buffer, 0, bytesRead);
			System.out.println("读取的的过程");
		}
		System.out.println();
		sock.close();
		fileIn.close();
		fileOut.close();
	}

	private static void sendBytes(Socket sock, FileInputStream fileIn) throws IOException {

		OutputStream sockOut = sock.getOutputStream();
		int bytesRead;
		byte[] buffer = new byte[BUFSIZE];
		while ((bytesRead = fileIn.read(buffer)) != -1){
			sockOut.write(buffer,0,bytesRead);
			System.out.println("写的过程");
		}
		//完成发送
		sock.shutdownOutput();
	}
	
}
总结:客户端关闭输出流 sock.shutdownOutput();

数据压缩协议

package com.tcp.ip.chapter4.cast;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;

public class CompressProtocol implements Runnable {

	public static final int BUFSIZE = 1024;
	
	private Socket clntSock;
	
	private Logger logger;
	public CompressProtocol(Socket clntSock, Logger logger){
		this.clntSock = clntSock;
		this.logger = logger;
	}
	public static void handleCompressClient(Socket clntSock, Logger logger){
		try {
			InputStream in = clntSock.getInputStream();
			GZIPOutputStream out = new GZIPOutputStream(clntSock.getOutputStream());
			
			byte[] buffer = new byte[BUFSIZE];
			int bytesRead;
			
			while ((bytesRead = in.read(buffer)) != -1) {
				out.write(buffer, 0, bytesRead);
				//全部刷出流
				out.finish();
				logger.info("客户端:" + clntSock.getRemoteSocketAddress() + " 完成");
			}
		}catch (IOException ex){
			logger.log(Level.WARNING, "异常在回显",ex);
			
		}
		try {
			clntSock.close();
		} catch (IOException e) {
			logger.info("Exception = " + e.getMessage());
		}
	}
	public void run() {

		handleCompressClient(this.clntSock, this.logger);
	}

}

总结:学习压缩类GZIPOutputStream

主要记录自己学习过程

你可能感兴趣的:(Java,TCP,IP,Socket编程学习笔记)