상세 컨텐츠

본문 제목

간단한 자바 소켓 프로그래밍 관련

본문

반응형

 

 

 

 

=================================

=================================

=================================

 

 

 

 

 

 

 

 

 

출처: http://androidgamedevs.blogspot.kr/2015/01/android-java-tcpip-clientserver-socket.html?m=1

Android Game Development

 

2015년 1월 13일 화요일

안드로이드 서버 클라이언트 소켓 통신 예제 (Android Java TCP/IP Client/Server Socket Example)

 

안드로이드 서버 클라이언트 소켓 통신 예제 (Android Java TCP/IP Client/Server Socket Communication Example) 


간단한 통신 프로그램을 짜보려고 인터넷을 다 뒤져봤지만, 제대로 구현된 예제 프로그램을 찾기가 어려웠다.

단순한 에코 프로그램 (echo server) 정도의 구현은 아주 쉽게 작성이 가능하지만, 화면 인터페이스 (UI)에 맞게 사용자가 입력한 내용을 전송한다거나, 소켓에서 받은 데이터로 TextView에라도 표시하려 하면 여러가지 문제에 봉착하고 만다. 실전에 사용하려 하면 어떤 문제들이 생길까? 왜 이런 문제들을 해결해 놓은 예제 프로그램은 찾을 수 없는 것일까?


A. 규칙! 이제 안드로이드의 모든 Network Function은 Main Thread (UI Thread)에서 실행하면 안된다. 



E/AndroidRuntime(673): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example/com.example.ExampleActivity}:android.os.NetworkOnMainThreadException
별 생각없이 Socket 함수들을 호출하면 이런 에러를 만나고 만다. 메인 쓰레드에서 네트워크기능을 사용할 수 없다는 뜻이다. 모든 네트워크 기능은 별도의 Thread에서 처리해야 한다. 하지만 여기서부가 또다른 골치아픈 문제의 시작점인것을...



B. 규칙! 메인 쓰레드 (Main Thread = UI Thread)가 아니면 UI를 변경해서는 안된다. 

 

내가 만든 쓰레드에서 직접 UI의 내용변경을 하면 안되고, 반드시 메인쓰레드가 처리하도록 요청하여야 한다. 이것을 구현하는 방법은 여러가지가 있는데,

a. activity.runOnUiThread()를 이용하는 방법
b. view.post()를 이용하는 방법
c. AsyncTask를 이용하는 방법
d. Message Handler를 이용하는 방법
e. ...

보통의 프로그램이라면 취향껏 고르면 될 일이다. 하지만 소켓 통신 프로그램은 그렇지 못하다!!

왜냐면, 소켓이 이미 하나의 Thread이고, 소켓에서 들어온 데이터를 계속 처리해야 하기 때문에 이 녀석 역시 Thread일 수 밖에 없기 때문이다. 그래서 규칙A와 B가 서로를 방해하는 형태로 계속 꼬여 버린다.

삽질끝에 얻은 가장 단순한 처리는 모든 소켓처리를 1개의 Thread에서 처리하고, UI 와의 통신은 Message Handler를 이용하는 것.

이렇게 구현된 SimpleSocket 클래스는 정말 아주 심플하다!!

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;

import android.os.Handler;
import android.os.Message;
import android.util.Log;

public class SimpleSocket extends Thread {
	private Socket  mSocket;

	private BufferedReader buffRecv;
	private BufferedWriter buffSend;

	private String  mAddr = "localhost";
	private int     mPort = 8080;
	private boolean mConnected = false;
	private Handler mHandler = null;

	static class MessageTypeClass {
		public static final int SIMSOCK_CONNECTED = 1;
		public static final int SIMSOCK_DATA = 2;
		public static final int SIMSOCK_DISCONNECTED = 3;
	};
	public enum MessageType { SIMSOCK_CONNECTED, SIMSOCK_DATA, SIMSOCK_DISCONNECTED };

	public SimpleSocket(String addr, int port, Handler handler) 
	{
		mAddr = addr;
		mPort = port;
		mHandler = handler;
	}

	private void makeMessage(MessageType what, Object obj)
	{
		Message msg = Message.obtain(); 
		msg.what = what.ordinal();
		msg.obj  = obj;
		mHandler.sendMessage(msg);
	}

	private boolean connect (String addr, int port) 
	{
		try {
			InetSocketAddress socketAddress  = new InetSocketAddress (InetAddress.getByName(addr), port); 
			mSocket = new Socket();
			mSocket.connect(socketAddress, 5000);
		} catch (IOException e) {
			System.out.println(e);
			e.printStackTrace();
			return false;
		}
		return true;
	} 

	@Override
	public void run() {
		if(! connect(mAddr, mPort)) return; // connect failed
		if(mSocket == null)         return;

		try {
			buffRecv = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
			buffSend = new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream()));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		mConnected = true;

		makeMessage(MessageType.SIMSOCK_CONNECTED, "");
		Log.d("SimpleSocket", "socket_thread loop started");

		String aLine = null;

		while( ! Thread.interrupted() ){ try {
			aLine = buffRecv.readLine(); 
			if(aLine != null) makeMessage(MessageType.SIMSOCK_DATA, aLine);
			else break;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}}

		makeMessage(MessageType.SIMSOCK_DISCONNECTED, "");
		Log.d("SimpleSocket", "socket_thread loop terminated");

		try {
			buffRecv.close(); 
			buffSend.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		mConnected = false;
	}

	synchronized public boolean isConnected(){
		return mConnected;
	}

	public void sendString(String str){
		PrintWriter out = new PrintWriter(buffSend, true);
		out.println(str);
	}
}


이 클래스를 사용하려면, 메세지를 처리할 Handler를 하나 구현하고, SimpleSocket을 생성한다음 start()만 해주면 된다. Looper.getMainLooper() 덕택에 UI 변경은 모두 메인쓰레드에서 처리할 수 있다.

public class ChatMain extends Activity {

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

		...
		ed1 = (EditText) findViewById(R.id.editText1);
		Button buttonSend = (Button) findViewById(R.id.button1);

		buttonSend.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				ssocket.sendString(ed1.getText());
				ed1.setText("");
			}
		});

		Handler mHandler = new Handler(Looper.getMainLooper()) {
			@Override
			public void handleMessage(Message inputMessage) {
				switch(inputMessage.what){
				case SimpleSocket.MessageType.SIMSOCK_DATA : 
					String msg = (String) inputMessage.obj;
					Log.d("OUT",  msg);
					// do something with UI
					break;

				case SimpleSocket.MessageType.SIMSOCK_CONNECTED : 
					// do something with UI
					break;

				case SimpleSocket.MessageType.SIMSOCK_DISCONNECTED : 
					// do something with UI
					break;

				}
			}    
		};  

		ssocket = new SimpleSocket("192.168.0.1", 8080, mHandler);
		ssocket.start();

		...

 

 

 

 

=================================

=================================

=================================

 

 

 

 

출처: http://blog.suminb.com/post/java-socket-programming-basics/

 

서론

이 글은 http://0pen.us 에 제가 썼던 글을 재구성하여 작성한 것입니다.

코드

먼저 간단한 예제를 보여드리겠습니다. 프로그램은 크게 두 부분으로 나뉩니다. 하나는 일을 간단하게 처리할 수 있도록 만들어놓은 래핑(wrapping) 클래스이고, 다른 하나는 그것을 이용하는 메인 클래스입니다.

래핑 클래스

/*
 * Worker.java
 *
 * Created on May 23, 2006, 10:36 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package app;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

/**
 *
 * @author superwtk
 */
public class Worker {

	private Socket socket;
	private BufferedInputStream bis;
	private BufferedOutputStream bos;

	/** Creates a new instance of Worker */
	public Worker() {
		socket = null;
		bis = null;
		bos = null;
	}

	public void connect(String host, int port) throws UnknownHostException, IOException {
		socket = new Socket(InetAddress.getByName(host), port);

		bis = new BufferedInputStream(socket.getInputStream());
		bos = new BufferedOutputStream(socket.getOutputStream());
	}

	public void connect(String host, int port, int localPort) throws UnknownHostException, IOException {
		socket = new Socket(InetAddress.getByName(host), port, InetAddress.getLocalHost(), localPort);
	}

	public void sendMessage(String message) throws IOException {
		bos.write(message.getBytes());
		bos.flush();
	}

	public String receiveMessage() throws IOException {
		byte[] buf = new byte[4096];
		StringBuffer strbuf = new StringBuffer(4096);

		int read = 0;
		while((read = bis.read(buf)) > 0) {
			strbuf.append(new String(buf, 0, read));
		}

		return new String(strbuf);
	}

	public void disconnect() throws IOException {
		bis.close();
		bos.close();
		socket.close();
	}

}

메인 클래스

/*
 * Main.java
 *
 * Created on May 23, 2006, 10:35 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package app;

/**
 *
 * @author superwtk
 */
public class Main {

	/** Creates a new instance of Main */
	public Main() {
	}

	/**
	 * @param args the command line arguments
	 */
	public static void main(String[] args) {
		try {
			Worker w = new Worker();
			w.connect("0pen.us", 80);

			StringBuffer message = new StringBuffer();
			message.append("GET /Forum/ HTTP/1.1\r\n");
			message.append("Host: 0pen.us\r\n");
			message.append("\r\n");

			w.sendMessage(new String(message));
			System.out.println(w.receiveMessage());

			w.disconnect();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}

}

http://0pne.us/Forum/ 의 HTML 코드가 출력됩니다.

GUI Version

 

그런데 스트림은 왜 쓰는것일까요?

자바에서 소켓을 이용한 통신은 일반적으로

  1. 소켓을 생성
  2. 연결 확립
  3. Input/Output 스트림 얻어오기
  4. 스트림을 통해서 데이터 주고 받기
  5. 스트림 닫기, 소켓 닫기

와 같은 순서로 이루어집니다.

Socket s = new Socket( /*생략*/ ); InputStream is = s.getInputStream(); OutputStream os = s.getOutputStream(); byte[] buf = new byte[1024]; is.read(buf); os.write("hooray~".getBytes()); is.close() os.close(); s.close();

물론 필요에 따라 순서를 바꿀 수도 있습니다. 그럼 소켓을 이용해서 직접 데이터를 주고받는 편한 방법을 놔두고 자바 소켓은 굳이 스트림을 이용해서 통신을 할까요?

Socket s = new Socket( /*생략*/ ); byte[] buf = new byte[1024]; s.recv(buf); s.send("hooray~".getBytes()); s.close();

스트림을 쓰지 않고 위와 같이 하면 좋을텐데 말입니다. 이건 유닉스나, 유닉스 구성하는 대부분의 소프트웨어를 만들어낸 C언어에서 가져온 개념이라고 생각됩니다.

유닉스에서는 모든 것이 파일로 표현됩니다. 일반적으로 말하는 (디스크 상의) 파일도 파일이고, 키보드, 마우스, 프린터, 스캐너 등 각종 디바이스들도 파일로 표현됩니다. 해당 파일에 뭔가를 쓰면 그 파일로 데이터가 출력됩니다. 예를 들면, 프린터를 나타내는 파일에 데이터를 쓰면 프린터로 해당 데이터가 출력되는 식입니다. 물론 마우스나 키보드를 나타내는 파일에 데이터를 쓸 수는 없겠지요.

stdio.h 에는 다음과 같은 함수가 있습니다.

ssize_t write(int fildes, const void *buf, size_t nbyte);

이 함수를 이용하면 어디든지 원하는 곳에 원하는 데이터를 쓸 수가 있습니다. 예를 들어서,

write(fileno(stdout), "I'm your father\n", 16);

와 같이 하면 stdout (표준출력) 을 통해서 해당 데이터를 출력합니다. 이번엔 로컬 디스크에 파일을 작성해보겠습니다.

FILE* file = fopen("output.txt", "w"); write(fileno(file), "I'm your father", 16);

이번엔 소켓을 이용해서 데이터를 출력(네트워크를 통해서 송출)해보겠습니다.

int sock = socket(AF_INET, SOCK_STREAM, 0); //...(생략) write(sock, "I'm your father", 16);

write 함수의 코드는 (거의) 똑같습니다. 하지만 완전히 다른 일을 하게 되죠. 하나는 표준입출력을 통해서 데이터를 출력하고, 다른 하나는 로컬 디스크에 출력을 하게 되고, 또 다른 하나는 네트워크를 통해서 데이터를 보내게 됩니다.

자바의 스트림도 이와 비슷한 개념입니다. FileInputStream, FileOutputStream, DataInputStream, DataOutputStream, BufferedInputStream, BufferedOutputStream, CipherInputStream, CipherOutputStream 등 수많은 클래스의 부모 클래스가 되는 InputStream 과 OutpuStream 클래스가 있습니다. 일반적인 경우, InputStream 의 read() 메소드와 OutputStream 의 write() 메소드만 있으면 입출력하는데 부족함이 없습니다. 수많은 종류의 스트림들은 이 부모 스트림들을 확장해서 특수한 목적에 사용되도록 특수한 성질을 부여한 스트림입니다. 예를 들어, Buffered{Input Output}Stream 은 내부적으로 적절한 버퍼링을 하여 입출력을 효율적으로 하도록 도와줍니다. Cipher{Input Output}Stream 은 객체 암호문을 쉽고 편하게 입출력 하도록 도와줍니다.

그럼 자바에서 유닉스의 파일 디스크립터와 같은 역할을 하는것은 무엇일까요. 각각의 InputStream 과 OutputStream 의 인스턴스들입니다. System.out 은 C의 stdout 이라고 봐도 무방할듯 합니다. 자바를 한번이라도 접해본 사람은 다음과 같은 코드를 본 적이 있을것입니다.

System.out.println("Hello World!");

이것은 다음의 코드와 같은 효과를 가집니다.

PrintStream ps = System.out; ps.println("Hello World!");

물론 다음과 같은 코드도 가능합니다.

OutputStream os = System.out; os.write("I'm your father".getBytes());

System.out 의 out 은 java.io.PrintStream 의 인스턴스로, java.lang.System 클래스에 static 멤버로 선언되어 있습니다. 만약 로컬 디스크에 파일을 쓰고 싶으면 다음과 같이 하면 됩니다.

OutputStream os = new FileOutputStream(new File("output.txt")); os.write("I'm your father".getBytes());

네트워크를 통해서 내보내고 싶다면 다음과 같이 하면 되겠죠.

Socket socket = new Socket(InetAddress.getByName("somewhere"), port); OutputStream os = socket.getOutputStream(); os.write("I'm your father".getBytes());

각 에제의 os.write() 코드는 완전히 똑같지만, 완전히 다른 결과를 가져옵니다. 스트림을 잘 이용하면 인생이 편해집니다.

 

 

 

=================================

=================================

=================================

 

 

 

출처: http://freemmer.tistory.com/61

 

 

출처 : http://www.javaservice.com/~java/bbs/write.cgi?m=&b=qna2&c=w_f&n=1221737969&p=1&s=t
 

안녕하세요 java와 c간의 TCPIP통신을 하고 있는데요 커넥트가 안되는 문제가 발생했었는데 그건 제 실수로 해결이 되었고 이번에 서로 통신을 해보니까 특이한 사항이 생겨서 질문을 드립니다. 구조는 에코 서버입니다. C가 서버이구요 자바가 클라이언트 입니다. 처음 서버가 열리구 클라이언트가 접속을 하고 자바에서 메세지를 입력받아서 서버에 전송을 하게 되어 있습니다. 같은 인디언체계에서 전송을 하기에 인디언 문제도 안생기구요 그런데 서버측에서는 accept 때와 매번 메세지가 전송이 될때 마다 특이한 문자가 들어옵니다. 혹시나 해서 아래처럼 반복문으로 바이너리 값을 찍어봤습니다. printf("[%02x]\n",(unsigned char)message[i]); ы [ac] [ed] [00] [05] thello? [74] [00] [07] [68] [65] [6c] [6c] [6f] [c0] [80] hijung? [74] [00] [08] [68] [69] [6a] [75] [6e] [67] [c0] [80] 이런식으로 나옵니다. 접속할때 클라이언트에게로 그대로 다시 전달해서 클라이언트에서 화면을 찍을때는 정상적인 값으로 보입니다. 일단 string 에다가 마지막에 "\0"을 붙여줬지만 안됩니다.(80이란값이 매번찍힘) ex) sendData + "\0" //sendData는 전송될 데이터를 가지고 있음 74 00 07 이런값이 뜨고 마지막에 07같은경우에는 전송된 문자열의 길이인거 같습니다. 클라이언트에서 스트림을 ObjectInputStream , ObjectOutputStream을 쓰고 있는데 여기 사이트에 자료를 조금 검색해보니 쓰면 안된다는 식으로 나왔던데요.. 제가 자바초보라서 저걸 안쓰면 어떤걸써서 적용을 시키는지...실제 적용을 할줄 몰라서;; 어떠한 문자열에 대한 차이 때문이거나 전송방식의 차이가 있는거 같은데 C에서는 앞부분도 문자열로 취급하고 출력하기에 가령 값을 저장한다든지에서 문제가 생길꺼 같은데요 이부분을 통일 시켜줄수 없을까요? 되도록이면 java단에서 고쳐야됩니다. 다른부분은 상사가 만들고 있어서요 ㅠ

제목 : Re: 그냥 OutputStream을 사용하시면 됩니다.
글쓴이: 너스(guest) 2008/09/19 09:30:27 조회수:914 줄수:26
 
 
제목 : Re: 다른 에러가 발생합니다.
글쓴이: 박정순(anaud2) 2008/09/19 15:39:43 조회수:1004 줄수:78
 
 
제목 : Re: 예제를 보면
글쓴이: 조정환(lovemini) 2008/09/25 17:20:52 조회수:939 줄수:20
 

 

저 예제만을 놓고 보면... bis.read(recv); 라고 하셨는데 recv 는 초기화 되어 있지 않습니다. 즉 null 상태이기 때문에 NullPointerException 이 발생합니다. recv = new byte[1024]; 와 같이 메모리 영역을 할당해 주세요. 한가지 덧붙이자면 BufferedInputStream 을 사용하신다면 위와 같이 하시지 마시고 String data = bis.readLine(); 로 하시면 한줄 한줄 읽어오므로 편리합니다. 단, 서버에서 LF 를 꼭 붙인다는 전제하에서.

 

 

 

=================================

=================================

=================================

 

 

반응형


관련글 더보기

댓글 영역