반응형

자바로 엑셀 읽기 쓰기등을 검색하다보면 수많은 글들이 나오는데 xls, xlsx를 보통 구분하여 생성하는 예제들을 확인할 수 있는데, 저는 어떤 엑셀파일을 올려도 읽도록 처리하고 싶었습니다.

 

실제로 운용중인 사이트에서도 엑셀을 업로드하여 읽고 DB에 저장하는 로직이 있는데, xls파일을 처리하도록 되어있는데 종종 xlsx로 올려서 문제가 되는 경우가 있었습니다.

 

이런 문제를 방지하기 위해 개발자 입장에서는 확장자를 체크하여 한가지의 엑셀만 받도록 처리하여도 되지만 사용자 입장에서는 불편 할 수도 있을것이라 생각했습니다.

 

.xls의 경우 97-03 통합문서로 과거의 엑셀파일이며, .xlsx는 요즘 엑셀문서를 저장하면 생성되는 확장자입니다.

POI라이브러리를 사용하여 읽거나 쓰기를 할때, 확장자에 따라 구분되어 사용되는데, 아래와 같다

.xls HSSF~
.xlsx XSSF~, SXSSF~(대용량 처리)

 

문서를 뜻하는 Workbook, Sheet, Row, Cell 모두 앞에 붙는 클래스명에 따라 구분되는데, 제일 부모 Interface를 사용하여 처리하도록 구성해봤습니다.

 

.xls, .xlsx 확장자에 따라 구분하여 읽기

간단하게 maven프로젝트를 구성하고 poi라이브러리를  maven에 추가합니다.

 

pom.xml

<dependency>
	<groupId>org.apache.poi</groupId>
	<artifactId>poi</artifactId>
	<version>4.1.0</version>
</dependency>
<dependency>
	<groupId>org.apache.poi</groupId>
	<artifactId>poi-ooxml</artifactId>
	<version>4.1.0</version>
</dependency>

HSSF, XSSF모두 사용하기 때문에 poi, poi-ooxml 모두 같은 버전으로 추가해줍니다.

(버전이 다른경우 에러가 발생합니다.)

 

App.java

package com.psw.excel;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

/**
 * POI LIB
 *
 */
public class App {
	public static void main(String[] args) {
		String path = "C:/whox2/";
		String fileName = "test.xlsx";

		List<Map<Object, Object>> excelData = readExcel(path, fileName);

		// 결과 확인!
		for (int i = 0; i < excelData.size(); i++) {
			System.out.println(excelData.get(i));
		}
	}

	public static List<Map<Object, Object>> readExcel(String path, String fileName) {
		List<Map<Object, Object>> list = new ArrayList<>();
		if (path == null || fileName == null) {
			return list;
		}

		FileInputStream is = null;
		File excel = new File(path + fileName);
		try {
			is = new FileInputStream(excel);
			Workbook workbook = null;
			if (fileName.endsWith(".xls")) {
				workbook = new HSSFWorkbook(is);
			} else if (fileName.endsWith(".xlsx")) {
				workbook = new XSSFWorkbook(is);
			}

			if (workbook != null) {
				int sheets = workbook.getNumberOfSheets();
				getSheet(workbook, sheets, list);
			}

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		return list;
	}

	public static void getSheet(Workbook workbook, int sheets, List<Map<Object, Object>> list) {
		for (int z = 0; z < sheets; z++) {
			Sheet sheet = workbook.getSheetAt(z);
			int rows = sheet.getLastRowNum();
			getRow(sheet, rows, list);
		}
	}

	public static void getRow(Sheet sheet, int rows, List<Map<Object, Object>> list) {
		for (int i = 0; i <= rows; i++) {
			Row row = sheet.getRow(i);
			if (row != null) {
				int cells = row.getPhysicalNumberOfCells();
				list.add(getCell(row, cells));
			}
		}
	}

	public static Map<Object, Object> getCell(Row row, int cells) {
		String[] columns = { "column1", "column2", "column3", "column4", "column5", "column6" };
		Map<Object, Object> map = new HashMap<>();
		for (int j = 0; j < cells; j++) {
			if (j >= columns.length) {
				break;
			}

			Cell cell = row.getCell(j);
			if (cell != null) {
				switch (cell.getCellType()) {
				case BLANK:
					map.put(columns[j], "");
					break;
				case STRING:
					map.put(columns[j], cell.getStringCellValue());
					break;
				case NUMERIC:
					if (DateUtil.isCellDateFormatted(cell)) {
						map.put(columns[j], cell.getDateCellValue());
					} else {
						map.put(columns[j], cell.getNumericCellValue());
					}
					break;
				case ERROR:
					map.put(columns[j], cell.getErrorCellValue());
					break;
				default:
					map.put(columns[j], "");
					break;
				}
			}
		}

		return map;
	}
}

각 셀의 타입도 체크하여 최대한 데이터에 맞게 집어넣을 수 있도록 처리해봤습니다.

주의점은 날짜형의 데이터도 NUMERIC으로 인식하는데 DateUtil을 통해 날짜타입인지  구분처리가 가능합니다.

 

 

결과 확인

먼저 test.xls 문서입니다.

정상적으로 읽어오는 것을 볼 수 있습니다.

 

 

 

다음은 test.xlsx입니다.

 

 

주의사항

이렇게 간단한 xls, xlsx를 읽는 소스를 작성하였지만, HSSF, XSSF의 정확한 내부 내용을 확인해보지 못했고, POI에서도 개발할때 굳이 구분을 지은 이유는 차이점이 있기 때문일것입니다. 상속을 통해 내부적으로 처리하는 로직이 다를 수 있고 이때문에 추후 문제가 발생할 수 있으니, 별도의 메소드를 사용시에는 많은 테스트가 필요할 수 있습니다.

반응형
반응형

myhappyman.tistory.com/191

 

Netty Client 튜토리얼 - 03 (서버에 연결이 안 될 경우 재시도하기)

myhappyman.tistory.com/189 Netty Client 튜토리얼 - 02 (메시지별 보내고 받고 끊기) myhappyman.tistory.com/187 Netty - Netty Client 튜토리얼 - 01 TCP 통신 해야하는 경우 JAVA SOCKET을 통해 서버를 구성..

myhappyman.tistory.com

 

NettyClient 튜토리얼 마지막 4장을 작성해볼까 합니다.

 

상대편(서버)에서 응답이 n초가 없는 경우 처리하기

bootstrap handler를 등록할 때 IdleStateHandler 라는 메소드를 등록하여 일정 시간동안 수신을 못받거나 응답을 못해주거나 할 때, Idle상태를 감지해주는 핸들러입니다.

 

idle상태 체크하기

bs.handler(new ChannelInitializer<SocketChannel>() {
	@Override
	protected void initChannel(SocketChannel ch) throws Exception {
    	//idle 등록
		ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0));
        
        
		NettyClientHandler handler = new NettyClientHandler(msgArr[idx]);		
		ch.pipeline().addLast("clientHandler", handler);
	}
});
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
	if(evt instanceof IdleStateEvent) {
		IdleStateEvent e = (IdleStateEvent) evt;
		if(e.state() == IdleState.READER_IDLE) {
        	//...처리할 동작
		}
	}
}

해당 핸들러에 등록된 상태가 만족되면 기존 등록하는 핸들러 중 userEventTriggered 메소드가 동작하게 되고 이 메소드에서 이벤트 종료를 받을 수 있는데 해당 이벤트가 IdleStateEvent의 참조 변수인지 확인 후 처리를 하시면 됩니다.

 

N초간 응답이 없어서 N번 전송 후 다음메시지로 넘기는 gif

 

IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds)

해당 핸들러 파라미터 옵션에 대하여 간단하게 알아보겠습니다.

 

각 파라미터 순서대로이며 해당 기능을 비활성하기 위해서는 '0'을 입력합니다.

1. readerIdleTimeSeconds : 읽기(수신) 시간으로 해당 시간만큼 동작하지 않으면 트리거가 발생합니다.

2. writerIdleTimeSeconds : 쓰기(발송) 시간으로 해당 시간만큼 동작하지 않으면 트리거가 발생합니다.

3. allIdleTimeSeconds : 읽기, 쓰기 모두를 지정하며 해당 시간만큼 동작하지 않으면 트리거가 발생합니다.

 

 

 

 

아래는 완성된 총 NettyClient 튜토리얼 소스입니다.

 


 

NettyClientAction.java

package com.psw.socket.nettyPrj.netty.client;

public interface NettyClientAction {
	public void close(NettyClientHandler handler);
	public void receive(NettyClientHandler handler);
}

 

NettyClient.java

package com.psw.socket.nettyPrj.netty.client;

import java.net.InetSocketAddress;
import java.net.SocketAddress;

import org.apache.log4j.Logger;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;

/**
 * NettyClient
 * @author psw
 */
public class NettyClient {
	private static final Logger logger = Logger.getLogger(NettyClient.class);
	
	private Bootstrap bs = new Bootstrap();
	private SocketAddress addr_;
	private Channel channel_;
	private String[] msgArr;
	private int idx;
	private int fail_cnt = 0;
	private final int FAIL_COUNT_LIMIT = 3;
	private NioEventLoopGroup group;
	
	public NettyClient(SocketAddress addr, String[] msgArr) {
		this.addr_ = addr;
		this.msgArr = msgArr;
	}
	
	public NettyClient(String host, int port, String[] msgArr) {
		this(new InetSocketAddress(host, port), msgArr);
	}
	
	//실제로 동작시킬 메소드 Bootstrap 연결 옵션 설정 및 연결 처리
	public void run() {
		if(this.addr_ == null) {
			logger.error("주소 정보가 없습니다.");
		}else if(this.msgArr == null || this.msgArr.length == 0) {
			logger.error("보낼 메시지가 없습니다.");
		}
		
		group = new NioEventLoopGroup(3);
		bs.group(group)
		.channel(NioSocketChannel.class)
		.option(ChannelOption.SO_KEEPALIVE, true);
		
		doConnect();
	}
	
	private void doConnect() {
		handlerSet();
		
		bs.connect(addr_).addListener(new ChannelFutureListener() {
			public void operationComplete(ChannelFuture future) throws Exception {
				if(future.isSuccess()) {
					logger.info("연결 성공");
					logger.info(addr_ + " connect()");
					channel_ = future.channel();
				}else {
					future.channel().close(); //실패하면 기존 연결을 종료하고
					if(FAIL_COUNT_LIMIT > ++fail_cnt) {
						logger.info("연결 실패 " + fail_cnt + "/" + FAIL_COUNT_LIMIT);
						bs.connect(addr_).addListener(this); //재연결 처리를 한다.
					}else {
						logger.info(FAIL_COUNT_LIMIT + "회 연결 초과");
						bs.group().shutdownGracefully(); //eventLoop에 등록된 Thread를 종료 처리한다.
					}
				}
			}
		});
	}
	
	private void handlerSet() {
		if(bs != null) {
			bs.handler(new ChannelInitializer<SocketChannel>() {
				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0));
					
					NettyClientHandler handler = new NettyClientHandler(msgArr[idx]);
					handler.setCallBackClientHandler(new NettyClientAction() {

						public void close(NettyClientHandler handler) {
							//종료 처리 후 더 보낼게 존재한다면 기존 옵션으로 재 연결처리를 하는 콜백 메소드
							logger.info("===== 서버가 응답이 없음 강제 종료 처리 =====");
							closeAndContinue();
						}

						public void receive(NettyClientHandler handler) {
							//응답 받은 메시지 콜백 메소드
							String receiveMsg = handler.getReceiveMsg();
							logger.info("callBack receive : "+ receiveMsg);
							closeAndContinue();
						}
						
					});
					
					ch.pipeline().addLast("clientHandler", handler);
				}
			});
		}
	}
	
	private void closeAndContinue() {
		try {
			channel_.close().sync(); //현재의 채널을 일단 닫는다.
			if(msgArr.length > ++idx) { //보낼 메시지가 남았으면 재연결 처리
				doConnect(); 
			}else { //보낼 메시지가 없다면 종료
				bs.group().shutdownGracefully(); //eventLoop에 등록된 Thread를 종료 처리한다.
			}
			
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

 

NettyClientHandler.java

package com.psw.socket.nettyPrj.netty.client;

import org.apache.log4j.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

public class NettyClientHandler extends ChannelInboundHandlerAdapter{
	
	private static final Logger logger = Logger.getLogger(NettyClientHandler.class);
	
	private String sendMsg;
	private int lostCnt_ = 0;
	private final int LIMIT_COUNT = 3;
	
	public NettyClientHandler(String msg) {
		this.sendMsg = msg;
	}
	
	private NettyClientAction action_;
	public void setCallBackClientHandler(NettyClientAction action) {
		this.action_ = action;
	}
	
	private String receiveMsg;
	public String getReceiveMsg() {
		return this.receiveMsg;
	}
	
	@Override
	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
	}
	
	@Override
	public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
	}
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		logger.info("채널이 메시지 발송할 준비가 됨.");
		msgSend(ctx);
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) {
		logger.info("메시지를 받는 메소드.");
		ByteBuf buf = (ByteBuf)msg;
		int n = buf.readableBytes();
		if( n > 0 ) {
			byte[] b = new byte[n];
			buf.readBytes(b);
			//수신메시지 출력
			this.receiveMsg = new String( b );
			logger.info("handler 수신된 메시지 >" + this.receiveMsg);
			
			if(this.action_ != null) {
				action_.receive(NettyClientHandler.this);
			}
		}
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) {
		logger.info("메시지를 받는 동작이 끝나면 동작하는 메소드.");
	}
	
	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
		if(evt instanceof IdleStateEvent) {
			IdleStateEvent e = (IdleStateEvent) evt;
			if(e.state() == IdleState.READER_IDLE) {
				if(LIMIT_COUNT > ++lostCnt_) { //n번을 초과하지 않았기때문에 메시지 재발송
					msgSend(ctx);
				}else { //대기시간 n초를 n번 초과하여 서버와 연결된 채널을 끊음
					if(this.action_ != null) {
						action_.close(NettyClientHandler.this);
					}
				}
			}
		}
	}
	
	private void msgSend(ChannelHandlerContext ctx) {
		ByteBuf messageBuffer = Unpooled.buffer();
		messageBuffer.writeBytes(sendMsg.getBytes());
		ctx.writeAndFlush( messageBuffer ); //메시지를 발송하고 flush처리
		logger.info("발송 메시지 >" + sendMsg);
	}
}

 

App.java

package com.psw.socket.nettyPrj;

import org.apache.log4j.Logger;

import com.psw.socket.nettyPrj.netty.client.NettyClient;

public class App {
	private static final Logger logger = Logger.getLogger(App.class);
	
	public static void main( String[] args ){
		logger.info("======= Netty Client Test =======");
		String host = "127.0.0.1";
		int port = 15510;
		String[] msgArr = {"hello world\n", "hello world2\n", "hello world3\n"};
		
		new NettyClient(host, port, msgArr).run();
	}
}

 

동작 결과

총 동작 테스트

 

 

처음 네티를 적용해보면서 경험해본 기술들을 정리해보았습니다. 잘 못 된 부분이 있다면 피드백 부탁드립니다. :D

반응형
반응형

myhappyman.tistory.com/189

 

Netty Client 튜토리얼 - 02 (메시지별 보내고 받고 끊기)

myhappyman.tistory.com/187 Netty - Netty Client 튜토리얼 - 01 TCP 통신 해야하는 경우 JAVA SOCKET을 통해 서버를 구성하거나 클라이언트 소스를 구성하여 연결을 하고 메시지를 주고 받고 종료하고 데이터를..

myhappyman.tistory.com

2장에 메시지를 주고 받고 연결을 종료하는 로직을 구성하여 봤는데, 연결한 상태에서 서버측에서 응답이 없게 되면 무기한으로 채널이 열리는 상황이 생기게 될 것입니다.

이런 경우 일정시간 체크 후에도 응답이 없으면 닫도록 처리하는 방식

 

서버측이 닫혀있거나 네트워크 문제등으로 서버측에 연결이 안되었을경우 여러번 시도 하거나 시도회수가 초과하면 종료하는 처리를 해보겠습니다.

 

서버가 열릴 때까지 요청하기

ChannelFuture 인터페이스에서 I/O처리를 담당하고 이벤트 추가 또는 연결되었는지 실패하였는 등 메소드를 통해 결과를 알 수 있습니다.

private Bootstrap bs = new Bootstrap();

bs.group...
...
...

//옵션 처리가 끝난 후 연결처리
bs.connect(addr_).addListener(new ChannelFutureListener() {
	public void operationComplete(ChannelFuture future) throws Exception {
		if(future.isSuccess()) {
			logger.info("연결 성공");
			logger.info(addr_ + " connect()");
			channel_ = future.channel();
		}else {
			logger.info("연결 실패");
			future.channel().close(); //실패하면 기존 연결을 종료하고
			bs.connect(addr_).addListener(this); //재연결 처리를 한다.
		}
	}
});

addListener메소드를 통해 작업 리스너를 추가하고 ChannelFutureListener 인터페이스를 추가하여 콜백 메소드 operationComplete를 상속받습니다.

 

operationComplete 메소드는 연결 처리 후 서버와의 상태값을 얻어 올 수 있는데, isSuccess메소드의 return값이 true면 연결 성공, false면 연결 실패처리가 됩니다.

 

실패한 경우 기존 채널을 종료하고 재연결 시도를 하도록 구성하였습니다.

이후에는 서버가 동작할때까지 연결 시도를 하는 것을 볼 수 있습니다.

 

서버가 열릴때까지 시도를 한다.

 

n회미만까지 연결 시도하기

시도 횟수를 처리할 변수와 제한을 둘 상수를 추가하여 if문을 통해 처리가 가능합니다.

private int fail_cnt = 0;
private final int FAIL_COUNT_LIMIT = 3;

bs...
...
...

bs.connect(addr_).addListener(new ChannelFutureListener() {
	public void operationComplete(ChannelFuture future) throws Exception {
		if(future.isSuccess()) {
			logger.info("연결 성공");
			logger.info(addr_ + " connect()");
			channel_ = future.channel();
		}else {
			future.channel().close(); //실패하면 기존 연결을 종료하고
			if(FAIL_COUNT_LIMIT > ++fail_cnt) {
				logger.info("연결 실패 " + fail_cnt + "/" + FAIL_COUNT_LIMIT);
				bs.connect(addr_).addListener(this); //재연결 처리를 한다.
			}else {
				logger.info(FAIL_COUNT_LIMIT + "회 연결 초과");
				bs.group().shutdownGracefully(); //eventLoop에 등록된 Thread를 종료 처리한다.
			}
		}
	}
});

성공하였을 경우에는 연결처리를 하고, 실패하였을 경우 count를 체크하도록 하였습니다.

N회까지 시도 후 실패하면 더이상 요청하지 않는다.

반응형
반응형

파일을 생성 후, 전송을 해줘야하는 경우 sftp 프로토콜 방식을 사용하여 파일을 전송해줘야 하는 경우가 있는데, Jsch를 사용하면 쉽게 연결하여 전송 할 수 있고, 전송간의 상태등을 callback을 통해 받을 수 있습니다.

 

SFtp 전송하기

먼저 jsch를 추가합니다.

<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>

 

App.java

package com.psw.file.sftp;

import org.apache.log4j.Logger;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;

public class App {
	
	private static final Logger logger = Logger.getLogger(App.class);
	
	private static String OUTPUT_FILE_PATH = "파일경로";
	private static String DESTINATION_PATH = "전송경로";
	private static String fileName = "파일명";
	private static String REMOTE_ADDR = "전송지 ip";
	private static int PORT = 포트;
	private static String USERNAME = "계정";
	private static String PASSWORD = "비밀번호";
	
	private static Session session = null;
	private static Channel channel = null;
	private static ChannelSftp channelSftp = null;
	
	public static void main( String[] args ){
		logger.info("======= sftp send start =======");
		JSch jsch = new JSch();
		try {
			session  = jsch.getSession(USERNAME, REMOTE_ADDR, PORT);
			session.setPassword(PASSWORD);
			java.util.Properties config = new java.util.Properties();
			config.put("StrictHostKeyChecking", "no");
			session.setConfig(config);
			session.connect();
			
			channel = session.openChannel("sftp");
			channel.connect();
			channelSftp = (ChannelSftp) channel;
			String filePath = OUTPUT_FILE_PATH + fileName;
			String DestinyPath = DESTINATION_PATH + fileName;
			channelSftp.put(filePath, DestinyPath);
		} catch (JSchException e) {
			logger.error("JSchException", e);
		} catch (SftpException e1) {
			logger.error("SftpException", e1);
		} finally {
			if(channelSftp != null) channelSftp.disconnect();
			if(channel != null) channel.disconnect();
			if(session != null) session.disconnect();
			
			logger.info("======= sftp send end =======");
		}
	}
}

접속시 호스트키값을 사용하지 않기에 config값에 StrictHostKeyChecking을 no 옵션으로 처리합니다.

(사용하실 분들은 yes)

 

연결할 채널은 sftp로 연결이므로 sftp로 열어주고 연결 합니다.

 

전송할 파일의 경로와 전송지 경로와 파일명 설정하고 전송을 시작합니다.

간단한 전송 예제를 확인해봤습니다.

 

 

진행상태 콜백 받기 외 옵션 설정

ChannelSftp 클래스에 대해 좀 더 알아보자면, 전송시 처리하는 put메소드 파라미터에 따라 옵션을 변경할 수 있고 전송진행상태를 확인 할 수 있습니다.

진행상태는 SftpProgressMonitor() 인터페이스를 상속받

put(src, dst) : 덮어쓰기

put(src, dst, 1) : 파일이 존재하면 동작하지 않음

put(src, dst, new SftpProgressMonitor(){
      ...
});

channelSftp.put(filePath, DestinyPath, new SftpProgressMonitor() {

	public void init(int op, String src, String dest, long max) {
		// TODO Auto-generated method stub
	}

	public boolean count(long count) {
		// TODO Auto-generated method stub
		return false;
	}

	public void end() {
		// TODO Auto-generated method stub
	}
});

 - init

   op : 전송 방향 (put or get)

   src : 전송 파일 이름

   dest : 대상 파일 이름

   max : 최종 개수

 

- count

  count : 지금까지 전송 된 바이트수

  계속 전송을 해야하면 return true, 취소해야하는경우 false 처리

- end

  모든 데이터가 전송되었거나 취소되면 호출 

 

 

진행상태 확인하기

package com.psw.file.sftp;

import org.apache.log4j.Logger;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;

public class App {
	
	private static final Logger logger = Logger.getLogger(App.class);
	
	private static String OUTPUT_FILE_PATH = "파일경로";
	private static String DESTINATION_PATH = "전송경로";
	private static String fileName = "파일명";
	private static String REMOTE_ADDR = "전송지 ip";
	private static int PORT = 포트;
	private static String USERNAME = "계정";
	private static String PASSWORD = "비밀번호";
	
	private static Session session = null;
	private static Channel channel = null;
	private static ChannelSftp channelSftp = null;
	
	public static void main( String[] args ){
		logger.info("======= sftp send start =======");
		JSch jsch = new JSch();
		
		try {
			session  = jsch.getSession(USERNAME, REMOTE_ADDR, PORT);
			session.setPassword(PASSWORD);
			java.util.Properties config = new java.util.Properties();
			config.put("StrictHostKeyChecking", "no");
			session.setConfig(config);
			session.connect();
			
			channel = session.openChannel("sftp");
			channel.connect();
			channelSftp = (ChannelSftp) channel;
			String filePath = OUTPUT_FILE_PATH + fileName;
			String DestinyPath = DESTINATION_PATH + fileName;
			channelSftp.put(filePath, DestinyPath, new SftpProgressMonitor() {
				long FileSize = 0;
				long sendFileSize = 0;
				int per = 0;
				public void init(int op, String src, String dest, long max) {
					this.FileSize = max;
				}
			
				public boolean count(long count) {
					this.sendFileSize += count;
					long p = this.sendFileSize * 100 / this.FileSize;
					if(p > per) {
//						System.out.println(per + "%");
						System.out.print("=");
						per++;
					}
					
					return true;
				}
			
				public void end() {
					// TODO Auto-generated method stub
				}
			});
		} catch (JSchException e) {
			logger.error("JSchException", e);
		} catch (SftpException e1) {
			logger.error("SftpException", e1);
		} finally {
			if(channelSftp != null) channelSftp.disconnect();
			if(channel != null) channel.disconnect();
			if(session != null) session.disconnect();
			System.out.println();
			logger.info("======= sftp send end =======");
		}
	}
}

반응형
반응형

myhappyman.tistory.com/187

 

Netty - Netty Client 튜토리얼 - 01

TCP 통신 해야하는 경우 JAVA SOCKET을 통해 서버를 구성하거나 클라이언트 소스를 구성하여 연결을 하고 메시지를 주고 받고 종료하고 데이터를 가공하거나 처리하거나 등등 여러가지를 할 수 있

myhappyman.tistory.com

NettyClient에 대해 포스팅을 진행하고 있는데, 첫번째 포스팅 글에서는 10개의 보낼 메시지가 있으면 한번 연결된 상태에서 10개의 메시지를 발신할때까지 핸들러에서 제어하면서 처리를 하였습니다.

 

다른 진행 방법으로 요청사항으로 메시지별 발송 -> 수신 -> 종료 형태로 반복하여 처리해달라고 하게 되면, 어떤식으로 처리를 해야 할지 알아보겠습니다.

 

연결 - 발신 - 수신 - 종료

 

NettyClient

1. NettyClientAction.java

package com.psw.socket.nettyPrj.netty.client;

public interface NettyClientAction {
	public void close(NettyClientHandler handler);
	public void receive(NettyClientHandler handler);
}

콜백 메소드 처리를 위해 interface 메소드 선언

 

2. NettyClient.java

package com.psw.socket.nettyPrj.netty.client;

import java.net.InetSocketAddress;
import java.net.SocketAddress;

import org.apache.log4j.Logger;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
 * NettyClient
 * @author psw
 */
public class NettyClient {
	private static final Logger logger = Logger.getLogger(NettyClient.class);
	
	private Bootstrap bs = new Bootstrap();
	private SocketAddress addr_;
	private Channel channel_;
	private String[] msgArr;
	private int idx;
	private NioEventLoopGroup group;
	
	public NettyClient(SocketAddress addr, String[] msgArr) {
		this.addr_ = addr;
		this.msgArr = msgArr;
	}
	
	public NettyClient(String host, int port, String[] msgArr) {
		this(new InetSocketAddress(host, port), msgArr);
	}
	
	//실제로 동작시킬 메소드 Bootstrap 연결 옵션 설정 및 연결 처리
	public void run() {
		if(this.addr_ == null) {
			logger.error("주소 정보가 없습니다.");
		}else if(this.msgArr == null || this.msgArr.length == 0) {
			logger.error("보낼 메시지가 없습니다.");
		}
		
		group = new NioEventLoopGroup(3);
		bs.group(group)
		.channel(NioSocketChannel.class)
		.option(ChannelOption.SO_KEEPALIVE, true);
		
		
		doConnect();
	}
	
	private void doConnect() {
		handlerSet();
		
		ChannelFuture f = bs.connect(addr_);
		channel_ = f.channel();
		logger.info(addr_ + " connect()");
	}
	
	private void handlerSet() {
		if(bs != null) {
			bs.handler(new ChannelInitializer<SocketChannel>() {
				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					NettyClientHandler handler = new NettyClientHandler(msgArr[idx]);
					handler.setCallBackClientHandler(new NettyClientAction() {

						public void close(NettyClientHandler handler) {
							//종료 처리 후 더 보낼게 존재한다면 기존 옵션으로 재 연결처리를 하는 콜백 메소드
							closeAndContinue();
						}

						public void receive(NettyClientHandler handler) {
							//응답 받은 메시지 콜백 메소드
							String receiveMsg = handler.getReceiveMsg();
							logger.info("callBack receive : "+ receiveMsg);
							closeAndContinue();
						}
						
					});
					
					ch.pipeline().addLast("clientHandler", handler);
				}
			});
		}
	}
	
	private void closeAndContinue() {
		try {
			channel_.close().sync(); //현재의 채널을 일단 닫는다.
			if(msgArr.length > ++idx) { //보낼 메시지가 남았으면 재연결 처리
				doConnect(); 
			}else { //보낼 메시지가 없다면 종료
				bs.group().shutdownGracefully(); //eventLoop에 등록된 Thread를 종료 처리한다.
			}
			
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

- 생성자를 통해 연결지의 IP, PORT정보를 얻고 보낼 메시지 배열을 전달받습니다.

- run() 메소드를 통해 전송을 위한 옵션 설정을 진행합니다. - Bootstrap 옵션 처리 (옵션처리는 최초에만 한번 설정)

- 옵션 설정이 끝나면 doConnect메소드를 통해 연결처리를 진행합니다.

- 연결 전 handler부분에서 메소드가 동작 후 콜백 받을 메소드를 정의합니다.

- 연결

- 동작들이 일어나고 receive 콜백 메소드가 동작하면 closeAndContinue() 메소드에서 더 보낼지 멈출지 판단합니다.

  -> 더 이상 보낼 메시지가 없다면 설정한 그룹들의 EventGroup을 모두 종료하기 위해 shutdown처리를 합니다.

 

3. NettyClientHandler.java

package com.psw.socket.nettyPrj.netty.client;

import org.apache.log4j.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyClientHandler extends ChannelInboundHandlerAdapter{
	
	private static final Logger logger = Logger.getLogger(NettyClientHandler.class);
	
	private String sendMsg;
	public NettyClientHandler(String msg) {
		this.sendMsg = msg;
	}
	
	private NettyClientAction action_;
	public void setCallBackClientHandler(NettyClientAction action) {
		this.action_ = action;
	}
	
	private String receiveMsg;
	public String getReceiveMsg() {
		return this.receiveMsg;
	}
	
	@Override
	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
		logger.info("채널 등록");
	}
	
	@Override
	public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
		System.out.println("채널 연결이 종료됨.");
	}
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		logger.info("채널이 메시지 발송할 준비가 됨.");
		ByteBuf messageBuffer = Unpooled.buffer();
		messageBuffer.writeBytes(sendMsg.getBytes());
		ctx.writeAndFlush( messageBuffer ); //메시지를 발송하고 flush처리
		logger.info("발송 메시지 >" + sendMsg);
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) {
		logger.info("메시지를 받는 메소드.");
		ByteBuf buf = (ByteBuf)msg;
		int n = buf.readableBytes();
		if( n > 0 ) {
			byte[] b = new byte[n];
			buf.readBytes(b);
			//수신메시지 출력
			this.receiveMsg = new String( b );
			logger.info("handler 수신된 메시지 >" + this.receiveMsg);
			
			if(this.action_ != null) {
				action_.receive(NettyClientHandler.this);
			}
		}
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) {
		logger.info("메시지를 받는 동작이 끝나면 동작하는 메소드.");
	}
}

- 채널이 발신 준비가되면 active메소드가 동작하고 발송할 메시지를 전달합니다.

- 응답이 오면 메시지를 읽어들이고 지역변수 receiveMsg에 담아주고 콜백 메소드 receive를 호출합니다.

- receive가 동작되면서 getReceiveMsg 메소드를 통해 전달받은 값을 받고, 현재의 채널을 종료처리 합니다.

- 보낼 메시지가 없을때까지 반복합니다.

 

실행결과

메시지 첫번째의 응답이 오면 채널이 종료되고 재연결되면서 두번째 메시지를 보낸다.

 

 

반응형
반응형

작성한 프로그램이 정상적으로 수행이 되고 메모리 낭비는 없는지? 생성한 Thread가 잘 동작하다가 문제없이 삭제되는지 등 여러가지 JAVA에서 생성된 스펙을 확인하고자 할때, JConsole을 통해 좀 더 유용하게 확인 할 수 있습니다.

 

 

Netty Client를 통해 여러 곳에 메시지를 처리해야 하는 상황이였는데, 이부분을 생성자를 통해 각각 필요할때마다 불러와서 new 생성자를 통해 처리하였습니다.

 

Netty에는 EventLoop가 Thread로 동작하게 되어 있는데, 생성된 이 group들을 별도로 종료하지 않으면 was가 종료되기 전까지 계속 살아있게되고 이상태로 서비스가 운영된다면 추후에는 Memory Leak과 같은 현상이 발생하면서 was가 죽어버리는 끔찍한 일이 발생할 수 있습니다.

 

JConsole

1. JConsole 실행하기

- 설치한 자바 위치의 bin디렉토리에 JConsole이 있습니다.

- 환경변수를 잡았다면 cmd창에 JConsole을 입력합니다.

JConsole입력

실행해줍니다.

 

2. JConsole 확인할 Java 프로세스 선택하기

JConsole이 동작하고 확인할 java프로세스를 선택하라고 뜹니다.

여기서 확인하고 싶은 클릭하고 프로세스를 연결합니다.

 

3. 경고창 확인

경고창이 뜨면 Insecure connection을 누릅니다.

 

4. 원하는 탭에서 사용

여러가지 탭

메모리에서 현재 사용하는 메모리 사용량

Threads에서 동작중인 Thread리스트 등등 각각 원하는 탭에서 상태를 확인하시면 됩니다.

 

5. 저는 Thread가 정상적으로 동작하고 종료되는 모습을 확인하기 위해 Threads탭을 클릭합니다.

Thread개수와 사용중인 Thread리스트가 나오고 Thread를 클릭하면 상세정보가 나옵니다.

 

해당 소스에서는 shutdownGracefully(); 처리가 없는 상태로 nioEventLoopGroup이 사용할때마다 추가되고 있는 모습을 볼 수 있습니다.

 

6. shutdownGracefully();를 통해 Thread 종료 처리 후 확인하기

특정 행위가 만족되면서 new NettyClient에 의해 EventLoop가 생성으로 Threads에 항목이 추가되고, 추후 종료동작에서 Shutdown으로 인해 EventLoop에서 사라지는걸 볼 수 있습니다.

반응형
반응형

TCP 통신 해야하는 경우 JAVA SOCKET을 통해 서버를 구성하거나 클라이언트 소스를 구성하여 연결을 하고 메시지를 주고 받고 종료하고 데이터를 가공하거나 처리하거나 등등 여러가지를 할 수 있는데, 이를 좀 더 기능적으로 사용하기 쉽게 도와주는 네트워크 프레임워크로 Netty가 있습니다.

 

이번에 프로젝트를 하나 진행하면서 여러가지 요구사항을 보다 쉽게 수행하고 추후에 능률적으로 유지보수하기 위해 Netty 프레임워크를 통해 진행해본 과정을 튜토리얼 식으로 남겨볼까 합니다.

 

Spring 프로젝트에 서버와 클라이언트를 구성하여 필요한 경우 연동처리를 진행하였는데, 이번에는 Maven프로젝트에서 구성하도록 하겠습니다.

 

Netty Client

1. 먼저 메이븐 프로젝트를 생성합니다.

 

2. 생성 후 Netty와 Log처리를 위해 Maven추가를 합니다. (pom.xml)

<!-- Netty -->
<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.0.33.Final</version>
</dependency>

<!-- log4j -->
<dependency>
	<groupId>log4j</groupId>
	<artifactId>log4j</artifactId>
	<version>1.2.17</version>
</dependency>

pom.xml

 

3. 로그를 사용하기 위해 log4j 설정을 합니다.

# Root logger option
log4j.rootLogger=info, stdout, logfile

# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p (%C{2}:%L) - %m%n

# Direct log message to log file
log4j.appender.logfile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.logfile.DatePattern='.'yyyy-MM-dd
log4j.appender.logfile.File=C:/netty/logs/dailylog.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss}] %5p (%C{2} - %M:%L) - %m%n
log4j.appender.logfile.Append=true
log4j.appender.logfile.encoding=UTF-8

 

4. 옵션 설정과 동작시킬 client를 작성합니다 - NettyClient.java

package com.psw.socket.nettyPrj.netty.client;

import java.net.InetSocketAddress;
import java.net.SocketAddress;

import org.apache.log4j.Logger;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
 * NettyClient
 * @author psw
 */
public class NettyClient {
	private static final Logger logger = Logger.getLogger(NettyClient.class);
	
	private Bootstrap bs = new Bootstrap();
	private SocketAddress addr_;
	private String[] msgArr;
	
	public NettyClient(SocketAddress addr, String[] msgArr) {
		this.addr_ = addr;
		this.msgArr = msgArr;
	}
	
	public NettyClient(String host, int port, String[] msgArr) {
		this(new InetSocketAddress(host, port), msgArr);
	}
	
	//실제로 동작시킬 메소드 Bootstrap 연결 옵션 설정 및 연결 처리
	public void run() {
		if(this.addr_ == null) {
			logger.error("주소 정보가 없습니다.");
		}else if(this.msgArr == null || this.msgArr.length == 0) {
			logger.error("보낼 메시지가 없습니다.");
		}
		
		bs.group(new NioEventLoopGroup(3))
		.channel(NioSocketChannel.class)
		.option(ChannelOption.SO_KEEPALIVE, true)
		.handler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel ch) throws Exception {
				ch.pipeline().addLast("clientHandler", new NettyClientHandler(msgArr));
			}
		});
		
		ChannelFuture f = bs.connect(addr_);
		f.channel();
		logger.info(addr_ + " connect()");
	}
}

NettyClient 생성자를 통해 보낼 메시지 배열과 연결정보를 넘깁니다.

run()메소드를 통해 연결할 옵션을 설정하고 채널을 연결합니다.

 

5. 실질적으로 동작할 Handler를 작성합니다 - NettyClientHandler.java

package com.psw.socket.nettyPrj.netty.client;

import org.apache.log4j.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyClientHandler extends ChannelInboundHandlerAdapter{
	
	private static final Logger logger = Logger.getLogger(NettyClientHandler.class);
	
	private String[] msgArr;
	private int idx = 0;
	public NettyClientHandler(String[] msgArr) {
		this.msgArr = msgArr;
	}
	
	@Override
	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
		logger.info("채널 등록");
	}
	
	@Override
	public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
		System.out.println("채널 연결이 종료됨.");
	}
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		logger.info("채널이 메시지 발송할 준비가 됨.");
		sendMsg(ctx, this.idx);
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) {
		logger.info("메시지를 받는 메소드.");
		ByteBuf buf = (ByteBuf)msg;
		int n = buf.readableBytes();
		if( n > 0 ) {
			byte[] b = new byte[n];
			buf.readBytes(b);
			//수신메시지 출력
			String receiveMsg = new String( b );
			logger.info("수신된 메시지 >" + receiveMsg);
			
			//보낼 메시지가 없으면 연결 종료
			if(msgArr.length ==  ++this.idx) {
				channelClose(ctx);
			}else {
				sendMsg(ctx, this.idx);
			}
		}
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) {
		logger.info("메시지를 받는 동작이 끝나면 동작하는 메소드.");
	}
	
	private void sendMsg(ChannelHandlerContext ctx, int i) {
		String msg = msgArr[idx];
		ByteBuf messageBuffer = Unpooled.buffer();
		messageBuffer.writeBytes(msg.getBytes());
		ctx.writeAndFlush( messageBuffer ); //메시지를 발송하고 flush처리
		logger.info("발송 메시지 >" + msg);
	}
	
	private void channelClose(ChannelHandlerContext ctx) {
		logger.error("채널 연결 종료");
		ctx.close();
	}
}

handler는 ChannelInboundHandlerAdapter를 상속받아 사용하고 각 동작별 정의된 메소드를 통해 데이터를 전달하거나 받을 수 있습니다.

연결이 되자마자 전송할 메시지를 처리하고, read메소드에서는 서버에서 전송하는 메시지를 받습니다.

 

메소드별로 간단하게 정리를 해보겠습니다.

- channelRegistered : 채널이 등록되면 동작합니다. 등록만 되었지 입출력 이벤트는 동작하지 않습니다.

- channelUnregistered : 반대로 채널이 해제되는 경우 닫힌 경우 동작합니다.(본인이 종료하거나 서버측이 종료 됨.)

- channelActive : 채널에 입출력이 가능해지면 동작하는 메소드입니다.

- channelRead : 메시지가 수신되면 동작합니다.

- channelReadComplete : 메시지 수신이 끝나면 동작합니다. (더 이상 수신할 정보가 없을 경우)

- userEventTriggered : 유저가 커스텀하여 등록한 이벤트가 발생할 경우 동작합니다. (timeout 등)

- exceptionCaught : exception 발생

 

6. 동작 테스트 - App.java

package com.psw.socket.nettyPrj;

import org.apache.log4j.Logger;

import com.psw.socket.nettyPrj.netty.client.NettyClient;

public class App {
	private static final Logger logger = Logger.getLogger(App.class);
	
	public static void main( String[] args ){
		logger.info("======= Netty Client Test =======");
		String host = "127.0.0.1";
		int port = 15510;
		String[] msgArr = {"hello world\n", "hello world2\n", "hello world3\n"};
		
		
		new NettyClient(host, port, msgArr).run();
	}
}

저는 SockTest v3.0.0을 통해 테스트를 진행하였습니다.

 

클라이언트에서 메시지를 발송 테스트

hello world부터 hello world3까지 발송하는 모습을 볼 수 있습니다.

 

 

아래 URL에서 소켓테스트 프로그램을 받을 수 있습니다.

sourceforge.net/projects/sockettest/

 

SocketTest - Test My Socket

Download SocketTest - Test My Socket for free. SocketTest - powerful and small software tool for socket testing. It can create both TCP and UDP client or server.

sourceforge.net

 

반응형
반응형

데이터를 검증하는 과정에서 문자열에 존재하는 데이터가 숫자만 있어야 하는 경우가 필요했는데, 이후 substring등으로 특정 형태로 변경해야해서 특히 검증을 꼼꼼하게 진행했습니다.

 

아래와 같이 정규식을 활용하면 쉽게 확인이 가능합니다.

public class StringUtils {

	public static void main(String[] args) {
		final String REGEX = "[0-9]+";
		String test = "20201119173455"; //년월일시분초
		
		if(test.matches(REGEX)) {
			System.out.println("숫자만 있습니다.");
		}else {
			System.out.println("숫자외에 값이 존재합니다.");
		}
	}
}

 

 

배열을 통해 여러가지 데이터를 검증해보았습니다.

public class StringUtils {

	public static void main(String[] args) {
		final String REGEX = "[0-9]+";
		String[] testArr = {"20201119173455", "20201119 173455", "2020111917:34:55"};
		for(int i=0; i<testArr.length; i++) {
			if(testArr[i].matches(REGEX)) {
				System.out.println("숫자만 있습니다.");
			}else {
				System.out.println("숫자외에 값이 존재합니다.");
			}
		}
	}
}

공백만 존재하여도 else를 타면서 정상적으로 체크하는 모습을 볼 수 있습니다.

반응형