Socket 통신 개념잡기

Posted by : at

Category : study


소켓(Socket) 프로그래밍 이란?

Server와 Client가 특정 Port를 통해 실시간으로 양방향 통신을 하는 방식

Socket 연결은 TCP/IP 프로토콜을 기반으로 맺어진 네트워크 연결 방식입니다. 그리고 이러한 Socket 연결 방식으로 프로그래밍 하는 것을 소켓(Socket) 프로그래밍이라고 하는데, Socket 프로그래밍은 Server와 Client가 특정 Port를 통해 연결을 유지하고 있어 실시간으로 양방향 통신을 할 수 있는 방식입니다. Client만 필요한 경우에 요청을 보낼 수 있는 Http 프로그래밍과 달리 Socket 프로그래밍은 Server 역시 Client로 요청을 보낼 수 있으며, 계속 연결을 유지하는 연결지향형 방식이기 때문에 실시간 통신이 필요한 경우에 자주 사용됩니다.

소켓(Socket) 프로그래밍 특징

Server와 Client가 계속 연결을 유지하는 양방향 프로그래밍 방식이다. Server와 Client가 실시간으로 데이터를 주고받는 상황이 필요한 경우에 사용된다. 실시간 동영상 Streaming이나 온라인 게임 등과 같은 경우에 자주 사용된다.

java 에서 사용되는 소켓 통신 예제

public class App 
{	
	public static ClassPathXmlApplicationContext context;
	
	private static final Logger log = LoggerFactory.getLogger(App.class);
	
    public static void main( String[] args )
    {	
    	context = new org.springframework.context.support.ClassPathXmlApplicationContext("config/app.xml");
    	
    	boolean isDev = System.getProperty("spring.profiles.active") == null||"local".equals(System.getProperty("spring.profiles.active"))||"dev".equals(System.getProperty("spring.profiles.active"));
    	
    	if(isDev == true) {    		
    		log.debug("IS DEV START ------------------------------------------------------------------------------");
        	//TestRealTimeServer server = context.getBean(TestRealTimeServer.class);
        	RealTimeServer server = context.getBean(RealTimeServer.class);
    		server.run();
    	}
    	
    }
}

먼저 제일 처음 App 앱에서 RealTimeServer 객체를 생성 하여 구동될경우 Server 인스턴스가 생성 될수 있는 Property 설정값을을 재정의 한다.

package com.realtime.server;

@Component
public class RealTimeServer {

	private static final Logger log = LoggerFactory.getLogger(RealTimeServer.class);
	
	@Value("#{realtimeProp['server.port']}")
	private int _port;
	
	@Value("#{realtimeProp['profile.name']}")
	private String _profile;
	
	@Value("#{realtimeProp['command.file']}")
	private String _basename;	
	
	private ReloadableResourceBundleMessageSource messageSource;			// 2018.12
	
	private static ExecutorService pool = new ThreadPoolExecutor( 10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100, true ) );		// 스레드 풀 추가. 2018.12
	public static volatile List<String> mbrNoList = new ArrayList<>();					// 처리중 mbrNo 목록
	public static volatile Map<String, Runnable> collisionMap = new Hashtable<>();		// mbrNo 충돌로 대기중인 task 맵
	
	private final static String _LAST_MODIFY_DATE = "2018-08-30 13:36";	
	public static Date LASTEXEC_DATETIME = null;	
	
	private final static int _BODY_LENGTH  	    =  400;  // 데이터바디부
	private final static int _DATA_LENGTH  		= 1284;  // 데이터부의 총 길이
	
	public static int THREAD_COUNT = 0;
	public static final int MAX_THREAD_COUNT = 10;
	private static Object cntObj = new Object();
	
	private String state = "run";
	
	private ServerSocket serverSocket = null;
	private InputStream in = null;
	private BufferedReader br = null;
	private Socket socket = null;
	private StringBuffer sbRead = null;
	
	public void run() {
		
    	try { 
			serverSocket = new ServerSocket(_port); 

			// 서버 시작 알림
			log.debug(String.format("%s RealTimeServer Started : port = %s / %s", _profile, _port, _LAST_MODIFY_DATE));
    		
    		while(true) {
    		
    			// 연결대기
    			socket = serverSocket.accept();
	    			
	    		// 접속 연결 시간 저장
	    		LASTEXEC_DATETIME = new Date();
        		
        		log.debug("");
	    		log.debug("=========================================================");
	    		log.debug("Socket connection has established...... ");
	    		log.debug("=========================================================");
	    		log.debug("");


	    		in = socket.getInputStream();
	        	br = new BufferedReader(new InputStreamReader(in, Constants.ENCODE_TYPE));

	        	sbRead = new StringBuffer();

	        	String telegram = null;

	        	while(true) {

	        		try {
	        			int c;    			

	        			if((c = br.read())!=-1) {

	        				sbRead.append((char) c);

	        				byte[] b = (sbRead.toString()).getBytes(Constants.ENCODE_TYPE);    		    					

	        				if(_DATA_LENGTH == b.length) {

	        					telegram = new String(b, Constants.ENCODE_TYPE);            	    					

	        					// 데이터부만 남기고 삭제
	        					telegram = telegram.substring(_DATA_LENGTH-_BODY_LENGTH);

	        					// 데이터을 파일 형태로 저장
	        					FileLogUtil.write(telegram);

	        					// 데이터처리	    		
	        					setTelegram(telegram);

	        					sbRead.setLength(0); // 버퍼초기화							

	        					telegram = null;
	        				}
	        			} else {					// client 가 정상적으로 연결 종료 한 경우
	        				log.info("Client has gone.");
	        				break;		  
	        			}
	        		}
	        		catch (IOException ex) {		// client 와 연결이 예상치 않게 끊어 지는 경우...
	    				log.debug(ex.getMessage());
	    				break;
	    			} catch(Exception ex) {
	    				log.debug("ERROR has occured"+ex.getMessage());
	    				break;
	    			}
	        	}
	        	
	        	if (br!=null) {
					br.close();
					log.info("The BufferedReader has closed");
				}
				if (in!=null) {
					in.close();
					log.info("The InputStream has closed");
				}
				if (socket!=null) {
					socket.close();
					log.info("The Socket has closed");
				}

				if ( state.equals("stop") ) {		// 소켓 스트림 read 루프 종료 했는데 커맨드 stop 이면 메인 스레드 종료.
					break;
				}
    		}
		} catch (IOException e) {
			log.info("An Unused ServerSocket has closed.");
			log.error("Server Error >> "+e.getMessage());
		} catch (Exception e) {
    		e.printStackTrace();
    		log.debug("RealTimeServer.run() Exception >>>>>>>>>>>>>>>>>>>>>>>> "+e.getMessage());
		} finally {
			if (br != null)
				try {
					br.close();
				} catch (Exception e) {
					e.printStackTrace();
		    		log.error("RealTimeServer.run() Exception >>>>>>>>>>>>>>>>>>>>>>>> "+e.getMessage());
				}
		}

		log.info("");
		log.info("Realtime sales data receiving thread has terminated. ");
	}

	/**
	 * 자원 회수
	 */
	private void releaseSocketResources() {
		try {
			if (socket!=null && !socket.isClosed()) {
				socket.shutdownInput();
				socket.close();
				//log.info("소켓 해제 완료.");
			}
			
			if (serverSocket!=null ) {						// 서버 소켓 해제
				serverSocket.close();
				//log.info("서버소켓 해제 완료");
			}		

		} catch (IOException ex) {
			log.error("자원 회수 중 에러 발생 : {}",ex.getMessage());
		}
	}
	
	private void setTelegram(String telegram) throws Exception {
		
		byte[] bytes = telegram.getBytes(Constants.ENCODE_TYPE);
		
		String mbrNo = StringUtil.trimWhitespace(bytes, 3, 100);
		log.debug(String.format("THREAD_COUNT ---------------------------------------------------- %d", THREAD_COUNT));
		
		Runnable task = new SocketThread(telegram);
		
		do {
			String result = RealTimeServer.checkAndSubmitThread ( mbrNo, task );
			
			if (result.equals("COLLISION")) {			// mbrNo 충돌이면 collisionMap 에 보관하여 다음에 다시 처리 시도.
				synchronized(collisionMap) {
					collisionMap.put(attachUniqueKey(mbrNo), task);
				}
				log.info("###### mbrNo collision occured. This job will be kept in the collisionMap. map size : {}, mbrNo is {}", collisionMap.size(), mbrNo);
				break;
			} else if (result.equals("SUCCESS")) {		// 제대로 풀링 됐으면 종료
				break;
			} else { 									// 스레드 풀의 큐가 가득찬 경우 여기서 스레드 큐 빠질때 까지 재시도 2초 대기  (소켓 버퍼에는 쌓이겠지.. )
				log.info("######## The thread pool is full and waiting to be emptied. mbrNo : {}",mbrNo);
				Thread.sleep(2000);
			}
		} while(true);

	}

Socket 접속에 필요한 구조가 생성 되었다.

/* Socket Thread*/
package com.realtime.server;

public class SocketThread extends Thread {

	private static final Logger log = LoggerFactory.getLogger(SocketThread.class);
	
	private String _telegram = null;
	
	private static final String adminSMSNumber = "01099117307";
	
	public SocketThread() {}
	public SocketThread(String telegram) {
		_telegram = telegram;
	}
	
	
	public void run() {
		
		// 전문처리 실행
		//RealTimeServer.THREAD_COUNT++;
		RealTimeServer.addToThreadCount();
			
		
		try {			
			
			model = new TestModel(_telegram);
			
			// 이제   시작
			result = model.process();
			
			if(result.isSuccess() == true) {
				// 비용처리/취소 성공
				// 카카오알림톡 전송
				
				if(result.getCode() == AUTO_APPL_HDL_CD.S03 || result.getCode() == AUTO_APPL_HDL_CD.S04) {					
					
					try {
						
						
						// 원 대상 번호						
						log.debug("원 수신자 : >> "+result.getTestModel().getMemberInfo().getHpNo());
								
						String telNo = result.getTestModel().getMemberInfo().getHpNo();
						
						result.getTestModel().getMemberInfo().setHpNo(telNo);
						
						boolean isDev = System.getProperty("spring.profiles.active") == null||"local".equals(System.getProperty("spring.profiles.active"))||"dev".equals(System.getProperty("spring.profiles.active"));
						
						if(isDev) {
							// 개발모드 이면 관리자 전화번호로 셋팅
							result.getTestModel().getMemberInfo().setHpNo(adminSMSNumber);
						}
						
						
						KakaoMessage kakao = null;
						
						if(result.getCode() == AUTO_APPL_HDL_CD.S03) {
							kakao = new KakaoDeductMessage(result.getTestModel());
						}
						else if(result.getCode() == AUTO_APPL_HDL_CD.S04) {
							kakao = new KakaoDeductCancleMessage(result.getTestModel());
						}	
						
						kakao.send();
					}
					catch(Exception ex) {
						// 성공인데 차감/취소 말고 다른게 있다면 알리기
						SMSSender sms = new SMSSender();
						
						sms.setTo(adminSMSNumber);
						sms.setMessage("HASK Real Time KakaoError >> "+ex.getMessage());
						
						sms.send();
					}					
					
				}
				else {
					// 성공인데 차감/취소 말고 다른게 있다면 알리기
					SMSSender sms = new SMSSender();
					
					sms.setTo(adminSMSNumber);
					sms.setMessage("HASK Real Time Unknown >> "+result.getCode());
					
					sms.send();
				}
			}
			else {
				// 실패
				// 실패사유가 오류라면 관리자에게 SMS 발송
				if(result.getCode() == AUTO_APPL_HDL_CD.P03) {
					// 관리자 SMS 발송
					SMSSender sms = new SMSSender();
					
					sms.setTo(adminSMSNumber);
					sms.setMessage("HASK Real Time Error >> "+result.getMessage());
					
					sms.send();
				}
			}
			
		}
		catch (Exception e) {			
			
			result.setCode(AUTO_APPL_HDL_CD.XX);
			result.setMessage("Exception >> "+e.toString());
		}
		
		RealTimeServer.LASTEXEC_DATETIME = new Date(); // 실행된 마지막 시간을 기록
		
		RealTimeServer.subtractFromThreadCount();
		
		RealTimeServer.removeCi(model.getCi());			// ciList에서 작업 완료된 ci  삭제.

		//log.debug("("+RealTimeServer.THREAD_COUNT+") "+result.getMessage());
		log.debug("("+RealTimeServer.THREAD_COUNT+":"+RealTimeServer.getThreadPoolQueueSize()+") "+result.getMessage());
	}
	
}

/* app.xml*/
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/util
        http://www.springframework.org/schema/util/spring-util.xsd">
        
        <!-- 트랜잭션 매니저 어노테이션 활성화 -->
        <tx:annotation-driven transaction-manager="transactionManager" />
	
    	<!-- spring 컴포넌트 스캔 범위 지정 -->
        <context:component-scan base-package="com.realtime" />
        
        <import resource="datasource.xml" />
    
</beans>
/* datasouce.xml*/

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">

	<util:properties id="realtimeProp" location="classpath:config/${spring.profiles.active:local}.xml" />
	
	<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="#{realtimeProp['oracle.driver']}" />
        <property name="url" value="#{realtimeProp['oracle.url']}" />
        <property name="username" value="#{realtimeProp['oracle.username']}" />
        <property name="password" value="#{realtimeProp['oracle.password']}" />
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:config/mybatis.xml" />
        <property name="mapperLocations">
            <array>
                <value>classpath*:/sql/**/*.xml</value>
            </array>
        </property>
    </bean>

    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg ref="sqlSessionFactory"></constructor-arg>
    </bean>
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.realtime.dao" />
        <property name="sqlSessionTemplateBeanName" value="sqlSession" />
    </bean>
	
</beans>
/*local.xml*/
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>

	<entry key="profile.name">LOCAL</entry>
	
	<entry key="oracle.driver">net.sf.log4jdbc.DriverSpy</entry>
	<entry key="oracle.url">jdbc:log4jdbc:oracle:thin:@localhost</entry>
	<entry key="oracle.username">username</entry>
	<entry key="oracle.password">password</entry>
	
	<entry key="server.port">XXXX</entry>
	
	<entry key="telegram.filePath">C:\_workspace\_log</entry>
	
	<entry key="sms.send.test">true</entry>
	
	<entry key="test.telegram"></entry>

	<entry key="command.file">/command/command-local</entry>
		
</properties>
/* mybatis.xml */
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="jdbcTypeForNull" value="NULL" />
        <setting name="defaultExecutorType" value="REUSE"/> <!-- SIMPLE|REUSE|BATCH -->
        <setting name="defaultStatementTimeout" value="360000"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
	</settings>
	<typeAliases>
  		<package name="com.realtime.model"/>
	</typeAliases>
</configuration>
run.sh
export LANG="ko_KR.UTF-8"
JAVA_HOME=/vol3/jdk1.8
SERVER_HOME=/logichome/tbs/logs/server

cd $SERVER_HOME
nohup $JAVA_HOME/bin/java -jar -Dname=Real -Dlog4jdbc.dump.sql.maxlinelength=0 -Dfile.encoding=UTF-8 -Dspring.profiles.active=real -Dapp.log.home=$SERVER_HOME/logs $SERVER_HOME/com.realtime.jar > /dev/null 2>&1 &

// stop.sh
perl -pi -e 's/run/stop/g' ./command/command-real.properties
/* testRealTimeServer.java*/
package com.realtime.server;

@Component
public class TestRealTimeServer {

	private static final Logger log = LoggerFactory.getLogger(RealTimeServer.class);
	
	@Value("#{realtimeProp['server.port']}")
	private int _port;
	
	@Value("#{realtimeProp['profile.name']}")
	private String _profile;
	
	@Value("#{realtimeProp['test.telegram']}")
	private String telegram;
	
	private final static String _LAST_MODIFY_DATE = "2018-07-31 14:19";	
	
	public static Date LASTEXEC_DATETIME = null;
	
	private final static int _BODY_LENGTH  	    =  400;  // 전문바디부
	private final static int _DATA_LENGTH  		= 1284;  // 데이터부의 총 길이
	
	public static int THREAD_COUNT = 0;
	public static final int MAX_THREAD_COUNT = 10;
	
	public void run() { 
    	
    	try {
    		// 서버 시작 알림
    		log.debug(String.format("%s TestRealTimeServer Started : port = %s / %s", _profile, _port, _LAST_MODIFY_DATE));
    		            	    					
			// 전문부만 남기고 삭제
			telegram = telegram.substring(_DATA_LENGTH-_BODY_LENGTH);
			
    		// 전문을 파일 형태로 저장
			//FileLogUtil.write(telegram);
    		
			// 전문처리	    		
			setTelegram(telegram);
    	}
    	catch (Exception e) {
		}
    	
	}

	private void setTelegram(String telegram) throws InterruptedException {
		// 전문을 처리한다.	
		log.debug(String.format("THREAD_COUNT ---------------------------------------------------- %d", THREAD_COUNT));
		
		if(THREAD_COUNT <= MAX_THREAD_COUNT) {
			// 쓰레드의 수가 최대 수보다 작으면 쓰레드 생성
			Thread socketThread = new Thread(new SocketThread(telegram));
			socketThread.start();
		}
		else {
			Thread.sleep(1000);	
			setTelegram(telegram);
		}
	}
}

/*model */
public class TestModel {
	
	private static final Logger log = LoggerFactory.getLogger(TestModel.class);
	
	private TestService service;
	
	public TestModel() {
		
	}
	
	public TestModel(String telegramSource) throws UnsupportedEncodingException, IllegalArgumentException, IllegalAccessException {		
		
		byte[] bytes = telegramSource.getBytes(Constants.ENCODE_TYPE);
		
		Field[] fields = this.getClass().getDeclaredFields();
		
		for(Field field : fields) {
			
			field.setAccessible(true);						
			
			BindValue bindValue = field.getAnnotation(BindValue.class);
			
			if(bindValue != null) {
		
				int startPosition = bindValue.StartPosition();
				int length = bindValue.Length();
				String name = bindValue.Name();
				
				if(length > 0) {							
					if(field.getType().getCanonicalName().equalsIgnoreCase("int")) {
						field.set(this, StringUtil.trimWhitespaceToInt(bytes, startPosition,length));
					}
					else {
						field.set(this, StringUtil.trimWhitespace(bytes, startPosition,length));
					}
				}
				
				String value = field.get(this).toString();
				
				System.out.println(String.format("%s =  %s | %s(%s)", name, value, value.length(), length));
			}
		}
		
		// 서비스 할당
		service = App.context.getBean(TestService.class);
					
		// 분할, 할부가 아닌경우 고객사사업자번호+CI로 사용자 정보를 업데이트 함
		if(EnableDvied() == false && IsallPayType() == true) {
			
			memberInfo = service.getMemberInfo(ci, designCode);
		}
		
		// 수신매출을 저장		
		service.setData(this);
		
	}
	
	

About Ghosts
Ghosts

Hi I a Ghosts, a Web Developer and Designer.

Email : memories109@gmail.com

Website : https://ghostsdev.github.io/

About Ghosts

개인정보 공유, 포트폴리오 제작 , 블러그 관리, 하루 일상, 사진 갤러리

Star
Useful Links