728x90

그래프에서 하나의 노드로부터 각 노드까지의 최단 거리를 구하기 위한 알고리즘으로,

 

코딩 테스트의 경로 탐색 문제를 해결하기 위해 기본적으로 알아야 한다.

 

다익스트라와 달리 모든 노드 쌍에 관한 최단 거리를 구하는 '플로이드-워셜 알고리즘'이 있다.

(다익스트라를 각 노드에 대해 구하면 동일하게 모든 쌍에 관한 최단 거리를 구할 수 있다.)

 

동작

 

 dist[i] = min( dist[i], dist[j] + wieght[j][i] )

 

(i노드까지의 최단거리) = (현재 i 노드까지의 최단거리), (이전 노드 j까지의 최단거리 + j 에서 i까지 거리)

 

두 값중 작은 값을 취하면 된다. 위 공식을 시작 노드로부터 점진적으로 진행시키면 된다.

 

BFS와 같이 queue를 이용하여 다음 노드로 진행 시키는데, 우선 순위 큐(힙)을 사용하면

 

업데이트 횟수가 줄어들어 더 빠르게 진행할 수 있다. (여기서도 우선 순위 큐로 설명한다.)

예시 그래프의 다익스트라 알고리즘의 진행

먼저 최단 거리를 저장한 배열을 선언하고 시작 노드(0번)에 거리(시작이므로 0)를 표시한 뒤,

나머지 노드에 INF(무한한값, 보통 -1로 둔다.)으로 채워 넣고, queue에 시작 노드를 넣는다.

 

    1. queue에서 데이터를 꺼내어(0번) 연결된 모든 노드(1, 3번)의 거리를 계산하고 기록한 뒤,

       기록된 거리값으로 queue에 삽입한다.

    2. queue에서 데이터를 꺼내어(최소힙이므로 1번) 연결된 모드 노드(3번)의 거리를 계산하여 더 작으므로

       업데이트 한다. 업데이트 되었으므로 다시 queue에 삽입한다.

    3. queue에서 데이터를 꺼내고(3번) 연결된 모든 노드(2, 4번)의 거리 계산 후 기록한 뒤, queue에 삽입한다.

       구현에 따라 다르지만, 현재 queue에 거리4로 먼저 넣은 3번 노드가 있지만 거리3으로 구한 것이 최단이므로,

       무시된다. 최소힙으로 구현하는 이점이다.

    4. queue에서 데이터를 꺼내어(2번) 연결된 모든 노드(1번)의 거리를 계산하지만 이미 1번에 더 작은 값이 있으므로,

       무시된다. 또 데이터를 꺼내어(4번) 연결된 모든 노드(5번)에 대해 계산하여 갱신 시킨다. 그 후 queue에 넣는다.

    5. queue에서 데이터를 꺼내어(5번) 연결된 모든 노드(4번)에 대해 계산하지만 이미 작은 값이 있어 무시된다.

       queue에 더이상 데이터가 없으므로 알고리즘을 종료한다.

    6. 시작 노드(0번)으로부터 각 노드까지의 최단 거리가 다 구해졌다.

 

코드

public class graph {
	// 최소힙에 넣기 위해 정의한 (노드번호, 거리)쌍의 클래스
	private class _pair implements Comparable<_pair> {
		public int dst;
		public int weight;
		public _pair(int d, int w) {
			this.dst = d;
			this.weight = w;
		}

		// 비교를 위해 Override
		@Override
		public int compareTo(_pair target) {
			return this.weight <= target.weight ? -1 : 1;
		}
	}
    
	private ArrayList<_pair>[] adjList; // 인접 리스트
	private PriorityQueue<_pair> queue; // 우선 순위 큐
	private int[] shortestDist;         // 최단 거리 배열
	
	public graph(int n) {
		adjList = new ArrayList[n];
		shortestDist = new int[n];
		
		Arrays.fill(shortestDist, -1);
		for (int i = 0; i < n; i++) {
			adjList[i] = new ArrayList();
		}
		queue = new PriorityQueue<>();
	}
	
	// 간선 추가
	public void addEdge(int src, int dst, int w) {
		this.adjList[src].add(new _pair(dst, w));
	}
	
	// 다익스트라
	public void dijkstra(int start) {
		// 시작 노드 삽입, 거리 행렬 기록
		queue.offer(new _pair(start, 0));
		shortestDist[start] = 0;
		
		// 큐에 데이터가 없을 때까지
		while (!queue.isEmpty()) {
			_pair p = queue.poll();
			
			for (_pair adj : adjList[p.dst]) {
				int dist = adj.weight + p.weight;
				
				// 계산된 거리가 기록되어있는 거리보다 작거나
				// INF값( -1 )일 경우
				if (dist < shortestDist[adj.dst] || shortestDist[adj.dst] == -1) {
					shortestDist[adj.dst] = dist;
					
					queue.offer(new _pair(adj.dst, dist));
				}
			}
		}
	}
	
	// 최단 거리 배열 출력
	public void printDist() {
		for (int i : shortestDist) {
			System.out.print(i + " ");
		}
		System.out.println();
	}
}
public class Main {

	public static void main(String[] args) {
		graph g = new graph(6);
		
		g.addEdge(0, 1, 1);
		g.addEdge(0, 3, 4);
		g.addEdge(1, 3, 2);
		g.addEdge(2, 1, 1);
		g.addEdge(3, 2, 8);
		g.addEdge(3, 4, 8);
		g.addEdge(4, 2, 3);
		g.addEdge(4, 5, 2);
		g.addEdge(5, 4, 1);
		
		g.dijkstra(0);
		
		g.printDist();
	}
}
728x90
728x90

BFS

 

DFS는 연결된 노드 하나를 먼저 방문하는 것에 비해 BFS의 경우 연결된 노드 전체를 먼저 방문하는 것이 우선이 된다.

 

동작을 간단하게 보면 다음과 같다.

( 그래프는 DFS때와 동일한 무방향 그래프를 사용한다.)

 

예시 그래프의 BFS

2번 노드 부터 시작하면 먼저 큐에 2를 푸쉬해 둔다.

 

  1. poll하여 큐 최하단 노드를 꺼내어 방문 표시를 하고 출력한다.

  2. 해당 노드의 인접 노드중 미방문 노드를 전부 push한다.

  2. 큐가 빌때까지 1, 2번을 계속한다.

 

예시 그래프의 노드를 위 순서대로 진행하면 2 1 3 4 0가 된다.

 

코드

 

저번 dfs코드를 재귀호출이 아닌 스택을 직접적으로 사용할 경우 중복 제거를 했어야 함에 반해,

 

bfs의 특성상 중복 제거를 하지 않아도 결과는 동일하다.

import java.util.*;

public class graph {
	private int szVertex;
	private ArrayList<Integer> adjList[];
	private boolean visited[];
	private Queue<Integer> queue;				// for BFS
	
	public graph(int v) {
		szVertex = v;
		visited = new boolean[szVertex];		// default false
		adjList = new ArrayList[szVertex];
		for (int i = 0; i < szVertex; i++) {
			adjList[i] = new ArrayList();
		}
		queue = new LinkedList<Integer>();
	}
	
	public void addEdge(int from, int to) {
		// 무방향 그래프이므로 반대의 경우도 추가
		adjList[from].add(to);
		adjList[to].add(from);
	}
	
	// 재귀 호출로 구현할 수 있지만 queue을 이용하여 구현하였다
	public void BFS(int start) {
		queue.add(start);
		
		// queue가 비어질때까지 게속
		while (queue.peek() != null) {
			// queue에서 가장 오래전 삽입된 데이터를 꺼내어 방문 행렬에 표시하고 출력
			int current = queue.poll();
			
			// 미방문 정점의 경우
			if (!visited[current]) {
				visited[current] = true;
				System.out.print(current + " ");
				
				// 인접 리스트에서 미방문 정점의 경우 queue에 추가
				for (int i : adjList[current])
					if (!visited[i])
						queue.add(i);
			}
		}
		System.out.println();
	}
	
	// 첫노드가 주어지지 않을 경우 0번 부터
	public void BFS() {
		BFS(0);
	}
}
public class bfs {

	public static void main(String[] args) {
		graph g = new graph(5);
		
		g.addEdge(0, 1);
		g.addEdge(0, 4);
		g.addEdge(1, 2);
		g.addEdge(1, 3);
		g.addEdge(2, 3);
		g.addEdge(2, 4);
		
		g.BFS(2);
	}
}
728x90
728x90

 DFS는 그래프나 트리에서 탐색하는 대표적인 두가지 방법(DFS, BFS)중 하나로, 하위 혹은 연결된 노드 중 하나를

 

우선적으로 방문하는 형태이다. 여기에서는 무방향 그래프를 사용하여 알아 보도록 한다.

 

인접 행렬, 인접 리스트

 

 먼저 그래프의 구현에 앞서 인접 행렬과 인접 리스트가 있다.

 

인접 행렬의 경우 NxN의 행렬로(N은 노드 개수)로 표시하는 것으로, 다음과 같이 표시한다.

무방향 그래프의 인접 행렬

0, 1의 노드가 연결되어 있는 경우, adjArray[0][1] = 1로 표시한다.

 

무방향 그래프의 경우 인접 행렬의 대각선(푸른선)을 기준으로 뒤집어진 대칭을 이루고 있다.

 

현재 행렬값이 모두 1로 되어 있지만 가중치 그래프의 경우 행렬의 숫자를 바꾸면 된다.

 

장점

    구현이 비교적 간단하다.

    i, j노드의 연결을 확인할 경우 adjArray[i][j]로 바로 확인할 수 있어, O(1)의 시간복잡도를 가진다.

단점 :

    i 노드에 연결된 모든 노드를 확인하고자 할때 adjArray[i][0] ~ adjArray[i][N-1] 모두 확인해야해서

    O(N)의 시간복잡도를 가진다. 노드가 많고 간선(연결)이 적을때 비효율이다.

    공간복잡도도 O(N^2)로 메모리를 많이 차지한다.

 

 인접 리스트의 경우 연결된 노드들의 리스트로 다음과 같이 표현한다.

무방향 그래프의 인접 리스트

0, 1의 연결을 확인하고 싶은 경우 adjList[0] 리스트를 순회하여 1번이 있는 지 확인하면 된다.

 

무방향의 경우 인접 행렬과 동일하게 0, 1이 연결된다면 1, 0의 연결도 추가하여야 한다.

 

장점 :

    i와 연결된 모든 노드를 확인하고자 할때 최악의 경우(i가 모든 노드와 연결된 경우) O(N)이지만,

    대부분의 경우 항상 O(N)인 인접행렬보다 빠르다.

    공간복잡도도 간선의 개수를 E라 할때 최악의 경우를 제외하고 O(E) < O(N^2)이므로 낭비가 적다.

단점 :

    i, j의 연결을 확인하고 싶을때 adjList[i]를 순회하며 j가 있는지 봐야 하므로 O(N)으로 인접행렬보다 느리다.

 

DFS

 

dfs의 동작을 간단하게 보면 다음과 같다.

예시 그래프의 DFS

2번 노드 부터 시작하면 먼저 스택에 2를 푸쉬해 둔다.

 

  1. pop하여 스택 최상단 노드를 꺼내어 방문 표시를 하고 출력한다.

  2. 해당 노드의 인접 노드중 미방문 노드를 전부 push한다. 이때 스택에 중복 push는 없게 한다.

  2. 스택이 빌때까지 1, 2번을 계속한다.

 

예시 그래프의 노드를 위 순서대로 진행하면 2 1 0 3 4가 된다.

 

코드

 

함수 호출이 스택메모리를 사용하므로 재귀호출로도 구현할 수 있다.

 

이번 코드에서는 JAVA에서 지원하는 stack 컨테이너로 구현하였다.

 

스택에 중복 데이터에 대한 예외처리를 하지 않았기 때문에 탐색 순서가 다르게 나올 수 있다.

(스택에 들어있는지 확인하는 행렬 같은 거를 추가하면 쉽게 구현할 수 있다.)

import java.util.*;

public class graph {
	private int szVertex;
	private ArrayList<Integer> adjList[];
	private boolean visited[];
	private Stack<Integer> stack;				// for DFS
	
	public graph(int v) {
		szVertex = v;
		visited = new boolean[szVertex];		// default false
		adjList = new ArrayList[szVertex];
		for (int i = 0; i < szVertex; i++) {
			adjList[i] = new ArrayList();
		}
		stack = new Stack();
	}
	
	public void addEdge(int from, int to) {
		// 무방향 그래프이므로 반대의 경우도 추가
		adjList[from].add(to);
		adjList[to].add(from);
	}
	
	// 재귀 호출로 구현할 수 있지만 stack을 이용하여 구현하였다
	public void DFS(int start) {
		stack.push(start);
		
		// stack이 비어질때까지 게속
		while (!stack.isEmpty()) {
			// stack 최상단을 꺼내어 방문 행렬에 표시하고 출력
			int current = stack.pop();
			
			// 미방문 정점의 경우
			if (!visited[current]) {
				visited[current] = true;
				System.out.print(current + " ");
				
				// 인접 리스트에서 미방문 정점의 경우 스택에 추가
				for (int i : adjList[current])
					if (!visited[i])
						stack.push(i);
			}
		}
		System.out.println();
	}
	
	// 첫노드가 주어지지 않을 경우 0번 부터
	public void DFS() {
		DFS(0);
	}
}
public class dfs {

	public static void main(String[] args) {
		graph g = new graph(5);
		
		g.addEdge(0, 4);
		g.addEdge(1, 3);
		g.addEdge(2, 4);
		g.addEdge(2, 3);
		g.addEdge(1, 2);
		g.addEdge(1, 0);
		
		g.DFS(2);
	}
}
728x90

'프로그래밍 > Java' 카테고리의 다른 글

다익스트라(Dijkstra) 알고리즘  (0) 2019.10.26
너비 우선 탐색(BFS)  (0) 2019.10.21
[Android] 사설 SSL 인증서를 이용한 https 통신  (4) 2018.05.30
[JAVA] NFC 제어  (2) 2017.10.15
[Android] HCE를 이용한 NFC 통신  (8) 2017.10.15
728x90

과정은 인증서를 설정해주는 것 외에는 보통의 hHttpURLConnection을 이요한 통신과 흡사하다.


private HttpsURLConnection urlConnection;
    private String method;
    private SSLContext sslContext;

    public static int BUFFER_SIZE = 1024;

    public HttpsURLConnection getConnection(Context context, String url, String requestMethod) {
        try {
            setCertification(context);

            // URL 설정
            URL request_url = new URL(url);
            method = requestMethod;

            urlConnection = (HttpsURLConnection) request_url.openConnection();
            urlConnection.setRequestMethod(method);
            urlConnection.setReadTimeout(95 * 1000);
            urlConnection.setConnectTimeout(95 * 1000);
            urlConnection.setDoInput(true);

            if (requestMethod.equals("GET"))
            {
                urlConnection.setRequestProperty("Accept", "application/json");
                urlConnection.setRequestProperty("X-Environment", "android");
            } else {
                urlConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                urlConnection.setRequestProperty("X-Environment", "android");
            }

            urlConnection.setHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            });
            // 위에서 설정한 인증서 사용
            urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());

            urlConnection.connect();

        } catch(ConnectException ce) {
            // 커넥션 오류
            ce.printStackTrace();
            return null;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return urlConnection;
    }

주의점은 HttpURLConnection이 아닌 HttpsURLConnection이다. 's'가 붙어있다.


메소드는 HttpURLConnection을 상속하고 있어서 비슷하다.


private void setCertification(Context context) throws Exception { CertificateFactory cf = CertificateFactory.getInstance("X.509"); InputStream caInput = context.getResources().openRawResource(R.raw.nive_crt); Certificate ca; try { ca = cf.generateCertificate(caInput); System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN()); } finally { caInput.close(); } // Create a KeyStore containing our trusted CAs String keyStoreType = KeyStore.getDefaultType(); KeyStore keyStore = KeyStore.getInstance(keyStoreType); keyStore.load(null, null); keyStore.setCertificateEntry("ca", ca); // Create a TrustManager that trusts the CAs in our KeyStore String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); tmf.init(keyStore); // Create an SSLContext that uses our TrustManager sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), null); }

코드 원본 : https://developer.android.com/training/articles/security-ssl?hl=ko


구글 문서에서 제공하는 코드이다. 인증서 데이터는 /res/raw/ 에 두고 openRawResource()메소드로 가져오고 있다.


    public String request(String content) {
        try {
            if(method.equals("POST")) {
                // POST 일때만 실행
                OutputStream outputStream = urlConnection.getOutputStream();
                BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
                bufferedWriter.write(content);
                bufferedWriter.flush();
                bufferedWriter.close();
                outputStream.close();
            }

            String response = null;
            if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                response = readStream();
            }

            urlConnection.disconnect();

            return response;
        }
        catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    private String readStream() throws Exception {
        StringBuilder responseStringBuilder = new StringBuilder();

        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
        for (; ; ) {
            String stringLine = bufferedReader.readLine();
            if (stringLine == null) break;
            responseStringBuilder.append(stringLine + '\n');
        }
        bufferedReader.close();

        return responseStringBuilder.toString();
    }

    public void disconnect() {
        urlConnection.disconnect();
    }

요청, 응답 코드


728x90
728x90

이 코드는 이 코드를 위해 작성하였습니다.



java에서 smartcardio라이브러리를 이용하면 nfc제어를 쉽게 구현할 수 있다.


nfc리더기가 연결되어 있으면 터미널 초기화가 가능하다. 현재 사용중인 acr 1251 리더기는 PICC, SAM 두개의 터미널이 있는데


기본적으로 PICC를 사용한다. (SAM은 태그에 보호기술이 들어간 것에 사용)


IsCardPresent() 메소드는 리더기 위에 카드가 있는지 검사하며 있을 경우 터미널에 대한 채널을 GetCardAndOpenChannel() 메소드로 오픈한다.


SendCommand() 메소드를 통해 올려진 카드(혹은 핸드폰)에 데이터를 보낸다.


위 안드로이드 어플리케이션과 통신하기 위해 Select AID과정을 거치는데 그에 해당하는 바이트열이 selectCardAID()메소드에 있는 것이다.


NFCMain.java

package nfc_simple;

import javax.smartcardio.*;

import java.nio.ByteBuffer;
import java.util.*;

public class NFCMain {
    private static final String UNKNOWN_CMD_SW = "0000";
    private static final String SELECT_OK_SW = "9000";
    
    public void run() {
        CardTerminal terminal;
        CardChannel channel;
        
        while (true) {
            // 터미널 초기화
            try {
                terminal = InitializeTerminal();
                
                if(IsCardPresent(terminal)) {                                   // 리더기 위에 카드(핸드폰)가 있을 경우
                    channel = GetCardAndOpenChannel(terminal);
                    
                    String response = selectCardAID(channel);
                    
                    System.out.println(response);
                }
                
                Thread.sleep(2000);
                
            } catch (CardException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public CardTerminal InitializeTerminal() throws CardException { 
        // Get terminal 
        System.out.println("Searching for terminals..."); 
        CardTerminal terminal = null; 
        TerminalFactory factory = TerminalFactory.getDefault(); 
        List<CardTerminal> terminals = factory.terminals().list(); 
      
        //Print list of terminals 
        for(CardTerminal ter:terminals) { 
            System.out.println("Found: "  +ter.getName().toString()); 
            terminal = terminals.get(0);// We assume just one is connected 
        } 
     
        return terminal; 
    }
    
    public boolean IsCardPresent(CardTerminal terminal) throws CardException {
        System.out.println("Waiting for card...");
        
        boolean isCard = false;
        
        while (!isCard) {
            isCard = terminal.waitForCardPresent(0);
            if(isCard)
                System.out.println("Card was found! :-)");
        }
        
        return true;
    }
    
    public CardChannel GetCardAndOpenChannel(CardTerminal terminal) throws CardException {
        Card card = terminal.connect("*");
        CardChannel channel = card.getBasicChannel();
        
        byte[] baReadUID = new byte[5];
        baReadUID = new byte[]{(byte) 0xFF, (byte) 0xCA, (byte) 0x00, (byte) 0x00, (byte) 0x00};
        
        // tag의 uid (unique ID)를 얻은 후 출력
        System.out.println("UID : " + SendCommand(baReadUID, channel));
        
        return channel;
    }
    
    public String selectCardAID(CardChannel channel) {
        
        byte[] baSelectCardAID = new byte[11];
        baSelectCardAID = new byte[]{(byte) 0x00, (byte) 0xA4, (byte) 0x04, (byte) 0x00, (byte)0x05,(byte) 0xF2, (byte) 0x22, (byte) 0x22, (byte) 0x22, (byte) 0x22};
        
        return SendCommand(baSelectCardAID, channel);
    }
    
    public static String SendCommand(byte[] cmd, CardChannel channel) { 
        String response = "";
        byte[] baResp = new byte[258];
         
        ByteBuffer bufCmd = ByteBuffer.wrap(cmd);
        ByteBuffer bufResp = ByteBuffer.wrap(baResp);
         
        int output = 0;
         
        try { 
            output = channel.transmit(bufCmd, bufResp); 
        } catch(CardException ex){ 
            ex.printStackTrace(); 
        } 

        for (int i = 0; i < output; i++) {
            response += String.format("%02X", baResp[i]); 
        }
          
        return response;  
    }
}



728x90
728x90

HCE (host-based card emulation)을 이용하면 nfc 태그가 없더라도 nfc통신이 가능하다.



리더기는 acr 1251제품을 사용하였으며 리더기 제어에 관한 코드는 여기에 있다.


hce를 사용하기 위해서는 기본적으로 HostApduService를 상속받아야 하는데 이름에서 알 수 있듯이 Service이기 때문에

매니페스트 파일에 추가해 주어야 한다.


AndroidMainifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hyunseo.hce_sample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".CardService"
            android:exported="true"
            android:permission="android.permission.BIND_NFC_SERVICE">
            <!-- Intent filter indicating that we support card emulation. -->
            <intent-filter>
                <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
            <!-- Required XML configuration file, listing the AIDs that we are emulating cards
                 for. This defines what protocols our card emulation service supports. -->
            <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
                android:resource="@xml/aid_list"/>
        </service>
    </application>

    <uses-permission android:name="android.permission.NFC"/>

</manifest>


AID란 것이 존재하는데 여러 어플리케이션에서 어떤 어플리케이션과 nfc통신을 해야 하는지를 구분하기 위한 값으로 임의로 설정 가능하다.

매니페스트 파일에 aid값이 있는 xml파일을 명시해 주어야 한다. 또 android.permission.BIND_NFC_SERVICE권한을 꼭 추가해 주어야 한다.


MainActivity.java

package com.example.hyunseo.hce_sample; import android.content.Intent; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.TextView; import org.w3c.dom.Text; public class MainActivity extends AppCompatActivity { private TextView _tv; private Intent _cardService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); _tv = (TextView) findViewById(R.id._textView); _cardService = new Intent(this, CardService.class); startService(_cardService); } @Override protected void onDestroy() { stopService(_cardService); } public void onClickUpdate(View v) { _tv.setText("Count : " + Counter.GetCurrentCout()); } }


메인 액티비티에서 하는 일은 간단한데 사용자가 버튼을 누르면 Count클래스에 있는 cnt값을 화면에 표시한다.



CardService.java

package com.example.hyunseo.hce_sample; import android.content.Intent; import android.nfc.cardemulation.HostApduService; import android.os.Bundle; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.util.Log; import java.util.Arrays; public class CardService extends HostApduService { private static final String TAG = "CardService"; // AID for our loyalty card service. private static final String SAMPLE_LOYALTY_CARD_AID = "F222222222"; // ISO-DEP command HEADER for selecting an AID. // Format: [Class | Instruction | Parameter 1 | Parameter 2] private static final String SELECT_APDU_HEADER = "00A40400"; // "OK" status word sent in response to SELECT AID command (0x9000) private static final byte[] SELECT_OK_SW = HexStringToByteArray("9000"); // "UNKNOWN" status word sent in response to invalid APDU command (0x0000) private static final byte[] UNKNOWN_CMD_SW = HexStringToByteArray("0000"); private static final byte[] SELECT_APDU = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID); //////////////////////////////////////////////////////////////////////////////////// private Messenger _handler; @Override public int onStartCommand(Intent intent, int flags, int startId) { // UI thread 핸들 얻음 Bundle extras = intent.getExtras(); // 서비스를 앱 종료시까지 계속 실행상태로 return START_STICKY; } // 외부로부터의 APDU명령을 받았을때 호출 @Override public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) { if (Arrays.equals(SELECT_APDU, commandApdu)) { Log.i(TAG, "Application selected"); Counter.AddOne(); return SELECT_OK_SW; } else { return UNKNOWN_CMD_SW; } } // 연결을 잃었을때 호출됨 @Override public void onDeactivated(int reason) { Log.i(TAG, "Deactivated: " + reason); } public static byte[] BuildSelectApdu(String aid) { // Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH | DATA] return HexStringToByteArray(SELECT_APDU_HEADER + String.format("%02X", aid.length() / 2) + aid); } public static byte[] HexStringToByteArray(String s) throws IllegalArgumentException { int len = s.length(); if (len % 2 == 1) { throw new IllegalArgumentException("Hex string must have even number of characters"); } byte[] data = new byte[len / 2]; // Allocate 1 byte per 2 hex characters for (int i = 0; i < len; i += 2) { // Convert each character into a integer (base-16), then bit-shift into place data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); } return data; } }


이 클래스가 nfc통신을 담당할 클래스이다. 외부로부터 이 어플리케이션에 apdu 명령을 할 경우 processCommandApdu() 메소드가 호출되며


카운터 클래스의 Counter.AddOne() 메소드를 호출한다.


기본적으로 Select AID란 작업이 완료되면 리더기에서 핸드폰을 떼기 전까지는 이 어플리케이션에만 apdu명령이 들어온다.


현재 aid는 f2222222로 설정하였고 Select AID의 apdu헤더는 00A40400이며 aid의 값은 5바이트(바이트로 변환하기 때문에 16진수로 끊어서)이기 때문에


여기서의 Select AID의 apdu명령은 "00A40400 05 f2222222"의 바이트열로 구성된다. 외부 카드리더기로부터 저 명령이 왔을때부터 핸드폰을 리더기에서


뗄때까지 이 어플리케이션과 통신이 이루어진다. 위 코드에는 select aid외 작업을 하지 않지만 원하면 다른 바이트를 주고 받을 수 있다.


select aid가 완료된 이후에는 따로 헤더가 필요치 않는다. processCommandApdu()의 리턴값은 리더기에 보낼 바이트열 값이다.



Counter.java

package com.example.hyunseo.hce_sample; public class Counter { private static int _cnt = 0; public static void AddOne() {_cnt++;} public static int GetCurrentCout() {return _cnt;} }


카운터 클래스는 간단하게 UI thread와 Service 두곳에서 접근 가능하도록 static으로 구성하였다. 카드서비스에서 _cnt의 값을 올리면


메인액티비티에서 증가된 값을 표시한다.



aid_list.xml

<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/service_name"
    android:requireDeviceUnlock="false">

    <aid-group android:description="@string/card_title" android:category="other">
        <aid-filter android:name="F222222222"/>
    </aid-group>
</host-apdu-service>


aid가 적혀있다. 외부로부터 이 값을 이용하여 들어온다.



실행결과 :

리더기에 찍고 UPDATE버튼을 누르면 카운트값이 변경이 된다.


자세한 설명은 구글 개발자 페이지를 확인하시면 됩니다.

https://developer.android.com/samples/CardEmulation/index.html

728x90
728x90

나중에 쓸 것 같아서 (이러고 쓰지 않는다.) 대충 서버와 메시지를 주고 받을 수 있게 만들었다.


기본 화면 구성이다.



IP와 Port를 받아서 소켓을 생성하고 Input datas에 아무 문자열을 입력하면 보내면


에코서버에서 똑같은 문자열을 보내며 그 결과를 토스트객체를 통해 화면에 띄운다. 이게 다다. 매우 초라하다.


예외처리를 하지 않았기 때문에 소켓 생성 없이 SEND 버튼을 누르면 앱이 뻗는다. 꼭 미리 누르자.


AndroidManifest.xml 파일에 네트워킹 사용을 위해


=> <uses-permission android:name="android.permission.INTERNET"/>


이 퍼미션을 넣는 것을 잊으면 안된다.

참고 : 에코서버 코드



MainActivity.java

package com.example.hyunseo.testingapp;

import android.app.ProgressDialog;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.*;


public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button m_bCreateSocket;
    private Button m_bSend;
    private EditText m_etInputIP;
    private EditText m_etInputPort;
    private EditText m_etSendData;
    private ProgressDialog m_pdIsLoading;

    private SocketManager m_hSocketManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        m_etInputIP = (EditText) findViewById(R.id.editText_inputIP);
        m_etInputPort = (EditText) findViewById(R.id.editText_inputPort);
        m_etSendData = (EditText) findViewById(R.id.editText_sendData);
        m_pdIsLoading = new ProgressDialog(this);

        // 클릭 이벤트 리스너 등록
        m_bCreateSocket.setOnClickListener(this);
        m_bSend.setOnClickListener(this);
    }

    private Handler m_Handler = new Handler() {
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case 0: // 소켓 생성 완료
                    // 로딩화면 제거
                    m_pdIsLoading.dismiss();
                    break;
                case 1: // 데이터 수신 완료
                    // 수신 데이터 토스트로 띄움.
                    Toast.makeText(MainActivity.this, "server responded : " + msg.obj, Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    };

    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button_CreateSocket:
                String IP = m_etInputIP.getText().toString();
                int Port = Integer.parseInt(m_etInputPort.getText().toString());

                m_pdIsLoading.setProgressStyle(ProgressDialog.STYLE_SPINNER);
                m_pdIsLoading.setMessage("Loading..");
                m_pdIsLoading.show();

                m_hSocketManager = new SocketManager(IP, Port, m_Handler);
                break;

            case R.id.button_Send:
                m_hSocketManager.sendData(m_etSendData.getText().toString());
                break;
        }
    }
}


안드로이드에서는 아이스크림 샌드위치 버전부터(아마 맞음) 소켓 생성같은 네트워킹 작업들을 메인 UI 스레드에서는


못하도록 해놓았다. 그래서 SocketManager에서 별도의 스레드를 생성하여 네트워킹을 관리하며 메인 스레드에서는 핸들러를 통한


화면 갱신만을 하고 있다. 여기서 ProgressDialog는 흔히 "로딩중"을 표시하는 객체로



요 화면이다. 소켓 생성이 끝나면 핸들러를 통해 메시지를 받아 ProgressDialog를 제거한다.


SocketManager.java

package com.example.hyunseo.testingapp; import android.os.Handler; import android.os.Message; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.util.Iterator; public class SocketManager { private String IP; private int Port; private SocketChannel m_hSocketChannel; private Selector m_hSelector; private readDataThread m_readData; private sendDataThread m_sendData; private Handler m_handler; public SocketManager(String ip, int port, Handler h) { this.IP = ip; this.Port = port; this.m_handler = h; // thread objects의 작업 할당 및 초기화 m_readData = new readDataThread(); m_readData.start(); } // m_createSocket thread 안에서 실행 private void setSocket(String ip, int port) throws IOException { // selector 생성 m_hSelector = Selector.open(); // 채널 생성 m_hSocketChannel = SocketChannel.open(new InetSocketAddress(ip, port)); // 논블로킹 모드 설정 m_hSocketChannel.configureBlocking(false); // 소켓 채널을 selector에 등록 m_hSocketChannel.register(m_hSelector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE); } public void sendData(String data) { m_sendData = new sendDataThread(m_hSocketChannel, data); m_sendData.start(); } private void read(SelectionKey key) throws Exception { // SelectionKey로부터 소켓채널을 얻어온다. SocketChannel sc = (SocketChannel) key.channel(); // ByteBuffer를 생성한다. ByteBuffer buffer = ByteBuffer.allocate(1024); int read = 0; // 요청한 클라이언트의 소켓채널로부터 데이터를 읽어들인다. read = sc.read(buffer); buffer.flip(); String data = new String(); CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder(); data = decoder.decode(buffer).toString(); // 메시지 얻어오기 Message msg = m_handler.obtainMessage(); // 메시지 ID 설정 msg.what = 1; // 메시지 정보 설정3 (Object 형식) msg.obj = data; m_handler.sendMessage(msg); // 버퍼 메모리를 해제한다. clearBuffer(buffer); } private void clearBuffer(ByteBuffer buffer) { if (buffer != null) { buffer.clear(); buffer = null; } } /*********** inner thread classes **************/ public class sendDataThread extends Thread { private SocketChannel sdt_hSocketChannel; private String data; public sendDataThread(SocketChannel sc, String d) { sdt_hSocketChannel = sc; data = d; } public void run() { try { // 데이터 전송. sdt_hSocketChannel.write(ByteBuffer.wrap(data.getBytes())); } catch (Exception e1) { } } } public class readDataThread extends Thread { public readDataThread() { } public void run() { try { setSocket(IP, Port); } catch (IOException e) { e.printStackTrace(); } // 소켓 생성 완료를 메인UI스레드에 알림. m_handler.obtainMessage(); m_handler.sendEmptyMessage(0); // 데이터 읽기 시작. try { while(true) { // 셀렉터의 select() 메소드로 준비된 이벤트가 있는지 확인한다. m_hSelector.select(); // 셀렉터의 SelectoedSet에 저장된 준비된 이벤트들(SelectionKey)을 하나씩 처리한다. Iterator it = m_hSelector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); if (key.isReadable()) { // 이미 연결된 클라이언트가 메시지를 보낸경우... try { read(key); } catch (Exception e) { } } // 이미 처리한 이벤트므로 반드시 삭제해준다. it.remove(); } } } catch (Exception e) { } } } }


아주 중구난방이다.


기본 소켓은 블럭킹 방식이기 때문에  SocketChannel을 이용하여 논블럭킹 방식으로 소켓을 생성하였다.



크게 중요한 스레드 객체가 두개있다. 소켓 생성 후 소켓들이 등록된(클라이언트라 하나밖에 음슴) Selector를 돌며


읽을 수 있는 상태의 소켓이 있으면 데이터를 읽는 readDataThread와 단순히 서버로 데이터를 보내는 sendDataThread이다.


readDataThread에서 수신 가능한 소켓이 있으면 read() 메소드를 호출하여 핸들러를 통해 UI 스레드로 보내고


UI 스레드에서 Toast를 이용하여 화면에 띄운다.





728x90

+ Recent posts