반응형

Spotbugs는 자바 코드를 분석해서 버그 패턴을 찾아주고, 해결방안을 제시해주는 공개소프트웨어입니다. 시작하셨던 프로젝트들을 납품을 완료하셨거나 하기 직전인분들도 많을텐데, 납품하기전이나 올해 작성해 본 프로젝트를 자가검증을 위해 돌려보고 자신의 소스를 되돌아보는것도 괜찮은 것 같습니다.

 

해당 프로그램으로 프로젝트를 돌려면 NullPointException이 발생할 수 있는부분이나, Date Class를 Equals로 비교하고 있는 잘못된 문법 String Class를 '==' 비교연산자로 비교하는 문법 IO를 사용하고 닫지 않았거나 null체크도 하지 않고 강제로 close하는 문법등등 문제가 될 수 있는 요소들을 정리해서 등급별로 보여줍니다.

 

한국인터넷진흥원(a.k.a KISA)에서도 권고하고 있는 프로그램으로 무료이며 이클립스에서 간단하게 마켓에서 설치해서 사용하시면 됩니다.

 

Spotbugs사용하기

1. 이클립스를 실행하고 마켓에 들어갑니다.

 

2. Spotbugs를 검색하고 설치합니다.

 

 

3. 설치가 완료되면 검사하고 싶은 프로젝트 우클릭후 SpotBugs를 클릭하면 자동으로 검사가 진행됩니다.

 

 

4. 검사가 완료되면 항목별로 문제점을 확인할 수 있습니다.

 

 

5. SpotBugs Perspective로 가보시면 상세하기 프로젝트의 문제점을 등급별로 표기해줍니다.

내용을 확인하고 수정하시면 됩니다.

반응형
반응형

소스코드 설명이나 사용 예시를 보여주기 위해 포스팅을 하다보면 종종 소스를 올리는 경우가 있습니다. 과거의 티스토리에 비하면 코드 블럭도 제공하고 좋아졌지만, 웹 기능을 보여주기 위해 HTML, CSS, Javascript(jQuery) 등을 동시에 포스팅 하는 경우에는 소스만 3개의 코드블럭이 들어가서 가독성도 안좋고 스크롤 압박(?)도 생기게 됩니다.

 

또한, 사용예시를 바로 보여주고 싶은 경우에도 html모드를 들어가서 작성한 소스들을 조합하여 보여줄 수도 있지만 여간 복잡한게 아니고 꼬일염려도 존재하여 개인적으로 꺼려지는 작업인데, 이런 경우 CodePen을 사용하면 작은 영역안에서 탭을 통해 HTML, CSS, Javascript를 처리하고 사용 결과도 볼 수 있습니다.

 

블로그에 CodePen적용하기

1. 먼저 CodePen사이트를 접속하여 회원가입을 하고 로그인을 합니다.

codepen.io/

 

CodePen

An online code editor, learning environment, and community for front-end web development using HTML, CSS and JavaScript code snippets, projects, and web applications.

codepen.io

저는 Github계정이 존재해서 연결하여 사용중입니다.

 

 

2. 로그인 후 작성을 위해 Pen 메뉴를 클릭합니다.

 

 

3. 코드를 작성합니다.

 

 

4. 코드 작성이 끝나면 해당 소스의 제목을 입력하고 Save버튼으로 저장을 해줍니다. (중요!)

 

 

5. 저장 후 아래 하단에 embed 버튼을 클릭합니다.

 

 

6. 원하는 테마를 선택 후 아래 소스를 복사합니다.

저는 다크테마값으로 했습니다.

 

 

7. 소스를 복사하셨으면, 블로그로 돌아와서 상단메뉴에서 기본모드->HTML로 변경합니다.

 

 

8. HTML모드가 되면 아래처럼 검은창에서 HTML태그들이 보일텐데 원하는 위치에 복사한 소스를 넣습니다.

소스에 보시면 data-height 속성이 보이는데 해당 값이 포스팅글에서 보일 높이 값입니다. 옆에 존재하는 inner Style태그의 높이를 바꿔도 변경되지 않으니 꼭 높이 값을 바꾸고 싶다면 해당 값을 수정해주세요. (저는 530으로 사용 중입니다.)

 

 

9. 복사한 태그 삽입이 끝나면 완료를 합니다. 입력 할 내용이 남았다면 기본모드로 돌아오셔서 추가적으로 입력 후 완료를 합니다.

 

 

아래는 CodePen 예제 샘플입니다.

See the Pen test by myhappyman (@myhappyman) on CodePen.

 

반응형
반응형

myhappyman.tistory.com/172

 

Spring - 스프링 프로젝트에서 netty사용하기

기존에 운영중이던 프로젝트를 고도화 시키면서 특정 서비스단에서 전달된 값을 DB에 저장하고 저장된 정보를 바로 특정 소켓 서버로 전달을 해줘야 하는 기능을 작성해야 했습니다. 이미 tomcat

myhappyman.tistory.com

 

이전에 Controller부분에서 @PostConstruct 어노테이션을 활용하여 Thread를 생성하여 돌리고 서버를 따로 구동하는 형태로 작성하였는데, Bean등록을 하거나 @Component 어노테이션을 활용해서 등록하고 사용해보는 예제를 확인해보겠습니다.

Bean등록을 통한 설정

servlet-context.xml

<!-- Netty Server 등록 -->
<beans:bean id="nettyServer" class="package명.NettyServer" name="nettyServer"/>

정의한 NettyServer class파일을 등록하여 사용하면 됩니다.

 

@Component 어노테이션을 통한 설정

@Component
public class NettyServer {
    private static final Logger logger = Logger.getLogger(NettyServer.class);

    private final int SERVER_PORT = 15500;
    private final TestService testService;    

    @Autowired
    public NettyServer(TestService testService) {
        this.testService = testService;
    }

    private int SERVER_PORT;
    private ServerBootstrap sbs = new ServerBootstrap();
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    @PostConstruct
    public void run() {
        logger.info(" ============================= Netty Server Start ============================= ");
        bossGroup = new NioEventLoopGroup(20);
        workerGroup = new NioEventLoopGroup(20);

        sbs.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_TIMEOUT, 5 * 1000)
        .childOption(ChannelOption.SO_TIMEOUT, 5 * 1000)
        .childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 응답없는 read상태 확인하는 Handler
                ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(5, 0, 0)); //아이들 상태

                // 메시지 송신, 수신, 응답없는 이벤트등을 처리할 Handler
                ch.pipeline().addLast("socketServerHandler", new NettyServerHandler(testService) ); //직접 동작 할 핸들러
            }
        });

        doConnect();
    }

    private void doConnect() {
        //서버는 Listen상태로 기다려야하는데, 톰캣이 Timeout이 발생함
        //이를 방지하기 위해 Thread로 처리한다.
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    ChannelFuture future = sbs.bind(SERVER_PORT).sync();
                    future.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    logger.error("InterruptedException", e);
                }
            }
        }).start();
    }

    @PreDestroy
    public void serverDestroy() throws InterruptedException {
        logger.info("================ Netty BootStrapServer Destroy() ================");
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
        sbs.group().shutdownGracefully();
    }
}

xml설정이 어렵다면 작성한 classBean으로 등록해주는 @Component 어노테이션을 활용하면 보다 쉽게 설정이 가능합니다.

 

추가해주시고 service, dao등 설정이 필요한 부분은 생성자를 통해 주입해줍니다.

 

이후 @PostConstruct 어노테이션을 활용하여 ServerBootStrap 옵션을 설정하고 구동을 해줍니다.

doConnect()함수에서 쓰레드를 생성하고 특정 포트로 바인딩하도록 설정하였는데, @PostConstruct는 해당 함수가 끝날때까지 대기하도록 설계되어 있는데, 해당부분에서 Netty 서버는 Listen상태로 계속 기다리고 있게됩니다.

톰캣입장에서는 구동 완료되는 시간이 Timeout설정이 되어 있는데 Timeout시간이 될때까지 설정이 끝나질 않으니 Timeout 에러가 발생하게 됩니다. 이부분을 막기위해 Thread를 통해 별도로 구성을 했습니다.

 

마무리

아직 스프링부트 프로젝트를 제대로 해볼 일이 없어서 아직도 Legacy만 쓰고 있는데, xml의 설정이 늘 어렵고 복잡하며 헷갈린것 같습니다. 이번 글에서 주의할 점은 xml에서 Bean등록을 했다면 Component 어노테이션은 지워야하고, Component 어노테이션으로 등록을 했다면 xml에서 Bean등록을 삭제해주시면 됩니다. 둘 다 설정하여도 동작은 하겠지만 2중으로 등록되면서 오류가 발생할 수 있습니다.

반응형
반응형

Spring에서 자주 바뀌는 값을 변수값으로 지정하여 개발하게 되면, 값이 변경이 필요한 경우 class파일 자체가 변경되어야 하기에 번거롭게 되는데, 이런 경우를 대비해 프로퍼티 파일을 읽게하여 재기동만으로 설정된 옵션(DB 연결IP, PORT 등)이나 정보등을 변경된 값으로 처리하여 수정 할 수 있습니다.

 

일반적으로 프로퍼티는 xml 설정을 통해 설정 하고 처리 할 수 있는데, ssh터널링과 같은 방식을 통해 DB를 연결하거나 해야 할때 xml설정으로 프로퍼티를 읽게되면 구동 순서상 에러가 발생할 수 있습니다.

 

이런 경우 xml에 Bean등록된 설정파일로 읽기가 아닌 자바에서 처리를 하시면 됩니다.

 

 

Java로 프로퍼티 파일 읽기

run()메소드에서 외부 메소드를 호출하여 읽고 정보별로 데이터를 얻는 예제입니다.

 

프로젝트 구조

 

 

config.properties

 

@Component
public class Test {
	private static final Logger logger = Logger.getLogger(Test.class);
	
	private final TestService testService;
	
	@Autowired
	public Test(TestService testService) {
		this.testService = testService;
	}

	@PostConstruct
	public void run() {
		
		GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
		ConfigurableEnvironment env = ctx.getEnvironment();
		MutablePropertySources prop = env.getPropertySources();
		try {
			prop.addLast(new ResourcePropertySource("classpath:config.properties"));
		} catch (IOException e) {
			logger.error("IOException", e);
		}
		
		// 프로퍼티 정보 얻기
		String ip = env.getProperty("db.ip");
		String pw = env.getProperty("db.pw");
		
		logger.info("IP : " + ip);
		logger.info("pw : " + pw);
	}
}

 

반응형
반응형

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 메소드를 통해 전달받은 값을 받고, 현재의 채널을 종료처리 합니다.

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

 

실행결과

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

 

 

반응형