[Socket] Java Socket 통신
오늘은 자바의 소켓 통신에 대해 알아봅시당. 웬 갑자기 ㅈㅏ바 소켓 통신이냐고요..? 그러게여..ㅎ 사실 이 글은 저의 동기에게 바치는 헌정 글… 자바를..자바라… ㅋㅎ
What to do
목표는? Client — Server 구조로 Socket 통신하기!
언어는? JAVA!
이때 포인트는 byte 단위로 데이터를 보낼거라는 것!
왜 Object로 안보내고 바이트 단위로 변환해서 보내냐? 나중에 통신할 서버가 c++로 구현되어 있으니까요!
자자.. 여러분 그거 아셨어요? 클라이언트 — 서버 모두 Java로 구현되어 있을때는 그냥 Object로 던져도 상관 없지만, 상대방이 c++이면 어림도 없다는 사실!
Java의 Object를 c++에서 이해 못하고 c++의 구조체를 자바에서 이해 못하니까 서로 byte 단위로 정보를 주고 받아야합니다.. 흑흑달이… 심지어 c++ 구조체에서 정의된 크기만큼 비트도 채워서 보내야해… BigEndian이냐 LittleEndian이냐 Byte 순서도 고려해야해…
하지만 그건 나중에 생각하기로하고! 일단 먼저 클라이언트 — 서버 모두 자바로 구현한 다음에 아래와 같은 로직을 작성해 볼 겁니다. 왜냐면 Object에서 Byte로 변환해서 소켓 통신을 해보는게 목적이니까~~
- Client 소켓 만듦
- Person Object를 만듦
- 만든 Object를 byte로 변환 후 소켓을 통해 Server에 보냄
- Server에서 받은 byte를 그대로 Client에게 돌려 보내면, 해당 byte를 다시 Person Object로 변환
Let’s get started
1. Client 소켓 만듦
자자 Socket을 만들어보자고요~~ 소켓 그거 별거없어요. 그냥 생성하면 됩니다.
Socket socket = new Socket()
그럼 서버랑 연결은 어떻게 하냐! 서버쪽의 ip랑 port를 인자로 넣은 SocketAddress
프로퍼티를 만들고 이것을 인자로 넣는 connect 함수를 사용해주면 끗!
SocketAddress address = new InetSocketAddress(hostName, port);socket.connect(address);
2. Person Object를 만듦
class Person implements Serializable {
int age;
Person(int age) {
this.age= age;
}
}
쨘~ 속성으로 age
를 갖고있는 Person
class를 만들었습니다. 이때 Serializable
를 implements
하고 있는게 보이시나요? 이걸 해줘야 나중에 byte로 변환(직렬화)이 가능하답니다.
3. 만든 Object를 byte로 변환 후 소켓을 통해 Server에 보냄
자바 직렬화는 방법은 java.io.ObjectOutputStream
객체를 이용합니다. 자세한 코드는 여기서 발췌해왔습니다.
public static byte[] toByteArray (Object obj)
{
byte[] bytes = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();
oos.close();
bos.close();
bytes = bos.toByteArray();
}
catch (IOException ex) {
//TODO: Handle the exception
}
return bytes;
}
이제 해당 함수로 받아온 byte array를 소켓을 통해 보냅니다.
//생성한 person 객체를 byte array로 변환
byte[] data = toByteArray(person);//서버로 내보내기 위한 출력 스트림 뚫음
OutputStream os = socket.getOutputStream();//출력 스트림에 데이터 씀
os.write(data);//보냄
os.flush();
지금까지의 코드는 다음과 같습니다
4. Server에서 받은 byte를 그대로 Client에게 돌려 보내면, 해당 byte를 다시 Person Object로 변환
자 서버(아래에 해당 코드가 있어요)는 위에서 받은 byte array를 그대로 돌려보냅니다.
이제 서버에서 보내는 바이트를 받는 코드는 아래와 같아요.
//수신 버퍼의 최대 사이즈 지정
int maxBufferSize = 1024;//버퍼 생성
byte[] recvBuffer = new byte[maxBufferSize];//서버로부터 받기 위한 입력 스트림 뚫음
InputStream is = socket.getInputStream();//버퍼(recvBuffer) 인자로 넣어서 받음. 반환 값은 받아온 size
int nReadSize = is.read(recvBuffer);
돌려 받은 byte array를 편하게 사용하려면 다시 Person 객체로 돌려놔야겠죠?(역직렬화)
코드를 살펴봅시다. 마찬가지로 해당 코드는 여기서 발췌해왔습니다.
public static <T> T toObject (byte[] bytes, Class<T> type)
{
Object obj = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream (bytes);
ObjectInputStream ois = new ObjectInputStream (bis);
obj = ois.readObject();
}
catch (IOException ex) {
//TODO: Handle the exception
}
catch (ClassNotFoundException ex) {
//TODO: Handle the exception
}
return type.cast(obj);
}
발췌해온 코드와 조금 다른점은 해당 함수에서는 Object를 반환했다면, 저는 Generic
을 이용해서 parameter
로 type
을 넣게 했어요. 변환된 object
를 그대로 반환하는게 아니라 해당 type
으로 캐스팅해서 반환할수 있도록 말이죠!
자 이제 이렇게 쓰면 될거예요!
Person receivePerson = toObject(recvBuffer, Person.class);
전체 코드로 확인해볼까요?
출력 결과를 봅시다!
Person
객체를 생성할때 인자로 넣어준 3이 잘 찍혀있쥬? 인자 값을 다르게 주면 해당 값으로 프린트 될거예요
자 이제 밑에 있는 서버 코드 돌려 놓고 또 다른데서 클라이언트 코드 돌리면 여러분도 확인할 수 있습니다!
cf) 서버 코드
사실 서버 코드에서 socket
, stream
등등 전역변수로 설정하고 작업을 다 끝내고 나면열어놓은 다 close()
함수를 통해 닫아줘야해요. 하지만 가독성과 설명을 위해 전역변수가 아닌 지역변수로 변경했습니다. 지금은 핵심 로직만 집중하기로! 예외 사항 등이 고려된 기본 코드는 이곳에서 확인 할 수 있답니다. 저는 byte
단위로 작업을 처리해야했고, 한번이 아니라 계속 api 쏘면서 확인하고 싶었기 때문에 이곳에서 제가 필요한 대로 조금 바꾼것!
만약 소켓 안 닫아주면 이런 에러가 날 수도 있어요.. (경험담 ㅎ)
Address already in use: JVM_Bind
이미 해당 포트가 사용되고 있어서 이런 에러가 뜨는건데요! 그럼 해당하는 프로세스를 죽이면 되겠죠?ㅋㅎ pid 를 찾는법은 윈도우는 여기, 맥은 여기서 참고합시다.
pid를 찾았으면 CMD 창에서 아래의 명령어를 통해서 종료시킬 수 있습니다 (윈도우 기준)
TASKKILL /F /PID (PID 번호)
ㅋㅋㅋㅋㅋㅋㅋㅋ포트 닫는 대신 프로세스 킬해버리기…ㅋㅋㅋㅋㅋㅋㅋㅋ극단적인거 같긴 하지만 소켓 통신할때 이 글만 볼 건 아니잖아요?! 포트 닫고 이렇게 저렇ㄱㅔ 처리를 잘 한 글은 또 찾아보시길..! (무책임) 그럼 20000!