블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 


안드로이드 채팅 UI 만들기 #2 


나인패치 이미지를 이용한 채팅 버블


조대협 (http://bcho.tistory.com)


지난 글에서는 ListView를 이용하여 스크롤이 가능한 텍스트 기반의 간단한 채팅창을 만들어보았다.

이번글에는 채팅 메세지에 이미지로 채팅 버블을 입히는 방법을 알아보도록 한다.


채팅 버블 이미지를 입히는 방법이나 원리는 간단한데, 채팅 메세지를 출력하는 TextView에 백그라운드이미지를 입혀서 출력하면 된다. 그런데 여기서 문제가 생기는데, 채팅 메세지 버블의 크기는 메세지의 글자수에 따라 가변적으로 변경되기 때문에, 일반적인 이미지를 백그라운드로 넣어서 가로로 늘이거나 줄이게 되면 채팅창이 이상하게 가로로 늘어날 수 가 있다.. (아래 그림에서 가로로 늘렸을때 말꼬리 부분 삼각형이 원본과 다르게 늘어난것을 확인할 수 있다) 



< 원본 이미지 > 



<가로로 늘린 이미지 >



그래서 필요한 것이, 특정 부분만 늘어나게 하는 것이 필요한데. 이렇게 크기가 변경되어도 특정 구역만 늘어나게 하는 이미지를 나인패치 이미지 (9-patch image라고 한다.). 나인패치 이미지를 이용하여 말풍선을 느리게 되면, 말꼬리 부분은 늘어나지 않고 텍스트가 들어가는 영역만 늘어난다. 




< 나인패치 이미지를 가로로 늘린 경우> 


나인패치 이미지 만들기


나인 패치 이미지는 안드로이드 SDK에 내장된 draw9patch라는 도구를 이용해서 만들 수 있다.

보통 안드로이드 SDK 가 설치된 디렉토리인 ~/sdk/tools 아래 draw9patch라는 이름으로 저장되어 있다.

실행하면 아래와 같은 화면이 뜨는데, 

좌측은 일반 이미지를 나인패치 이미지로 만들기 위해서 늘어나는 영역을 지정하는 부분이고 (작업영역), 우측은 가로, 세로등으로 늘렸을때의 예상 화면 (프리뷰)을 보여주는 화면이다.




그러면 9 patch  이미지는 어떻게 정의가 될까? draw9patch에서 가이드 선을 드래그해서 상하좌우 4면에, 가이드 선을 지정할 수 있다.



좌측은 세로로 늘어나는 영역, 상단을 가로로 늘어나는 영역을 정의하고, 우측은 세로로 늘어났을때 늘어나는 부분에 채워지는 이미지를, 하단은 가로로 늘어났을때 채워지는 이미지 영역을 지정한다. 

이렇게 정의된 나인 패치 이미지는 어떻게 사용하는가? 일반 이미지 처럼 사용하면 되고, 크기를 조정하면 앞서 정의한데로, 늘어나는 부분만 늘어나게 된다.


나인패치 이미지를 채팅 메세지에 적용하기 


그러면 이 나인패치이미지를 앞에서 만든 채팅 리스트 UI에 적용하여 말 풍선을 만들어보도록 하자.

여기서는 채팅 버블을 좌측 우측용 양쪽으로 만들도록 하고, 서버에 연결된 테스트용이기 때문에, 메세지를 입력하면 무조건 좌/우 버블로 번갈아 가면서 출력하도록 한다.


앞의 코드 (http://bcho.tistory.com/1058) 에서 별도로 바위는 부분은 ChatMessageAdapter의 getView 메서드만 아래와 같이 수정하고 채팅 버블로 사용할 이미지를 ~/res/drawable/ 디렉토리 아래 저장해 놓으면 된다. 




그러면 수정된 getView 메서드를 살펴보도록 하자.


   @Override

    public View getView(int position, View convertView, ViewGroup parent) {

        View row = convertView;

        if (row == null) {

            // inflator를 생성하여, chatting_message.xml을 읽어서 View객체로 생성한다.

            LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            row = inflater.inflate(R.layout.chatting_message, parent, false);

        }


        // Array List에 들어 있는 채팅 문자열을 읽어

        ChatMessage msg = (ChatMessage) msgs.get(position);


        // Inflater를 이용해서 생성한 View에, ChatMessage를 삽입한다.

        TextView msgText = (TextView) row.findViewById(R.id.chatmessage);

        msgText.setText(msg.getMessage());

        msgText.setTextColor(Color.parseColor("#000000"));


        // 9 패치 이미지로 채팅 버블을 출력

        msgText.setBackground(this.getContext().getResources().getDrawable( (message_left ? R.drawable.bubble_b : R.drawable.bubble_a )));


        // 메세지를 번갈아 가면서 좌측,우측으로 출력

        LinearLayout chatMessageContainer = (LinearLayout)row.findViewById(R.id.chatmessage_container);

        int align;

        if(message_left) {

            align = Gravity.LEFT;

            message_left = false;

        }else{

            align = Gravity.RIGHT;

            message_left=true;

        }

        chatMessageContainer.setGravity(align);

        return row;


    }


수정 내용은 간단한데, 좌측/우측용 채팅 버블을 출력하는 부분과, 좌측 버블은 좌측 정렬을, 우측 버블은 우측 정렬을 하는 내용이다.


채팅 버블을 적용하는 방법은 TextView에서 간단하게 setBackground 메서드를 이용하여 백그라운드 이미지를 나인패치 이미지를 적용하면 된다.


msgText.setBackground(this.getContext().getResources().getDrawable( (message_left ? R.drawable.bubble_b : R.drawable.bubble_a )));


나인패치이미지가  resource아래 drawable 아래 저장되어 있기 때문에, getResource().getDrawable() 메서드를  이용하여 로딩 한다.


        LinearLayout chatMessageContainer = (LinearLayout)row.findViewById(R.id.chatmessage_container);

        int align;

        if(message_left) {

            align = Gravity.LEFT;

            message_left = false;

        }else{

            align = Gravity.RIGHT;

            message_left=true;

        }

        chatMessageContainer.setGravity(align);


다음으로는 채팅 버블을 번갈아 가면서 좌/우측에 위치 시켜야 하는데, 채팅 버블의 위치는 채팅 메세지를 담고 있는 LinearLayout을 가지고 온후에, LinearLayout의 Gravity를 좌우로 설정하면 된다.


나인패치 이미지를 이용한 완성된 채팅 버블 UI는 다음과 같다. 






안드로이드에서 ListView를 이용한 채팅 UI 만들기


조대협 (http://bcho.tistory.com)


안드로이드 프로그래밍 기본 개념이 어느정도 잡혀가기 시작하니, 몬가 만들어봐야겠다는 생각이 들어서 생각하던중에 결론 낸것이, 간단한 채팅 서비스, 기존에 node.js 하면서 웹용 채팅을 만들어보기도 했고, 찾아보니, 안드로이드용 SocketIO 라이브러리도 잘되어 있어서 서버 연계도 어려울것이 없을것 같고, 또한 메세지가 왔을때 푸쉬 알림을 써야 하는 등 이것저것 실습이 될것 같아서, 결국은 채팅으로 정했다.


서버나 연계 코드 구현보다, 가장 어려운게 역시나 UI 디자인과 프로그래밍인데, 가장 쉬운 방법으로는 ListView를 사용하는 방법이 무난하다. (결국 코딩을 하고 나니 여러가지 한계를 느껴서 다른  UI를 찾으려고 하고는 있지만)


궁극적으로 만들고자 하는 UI는 카카오톡 처럼 말풍선이 나오는 UI이다. 





말풍선은 Ninepatch (나인패치) 이미지 라는 것으로 만들 수 가 있는데, 나인 패치 이미지에 대해서는 나중에 알아보도록 하고, 여기서는 말풍선 없이, 화면에 스크롤되서 내려가는 텍스트 기반의 대화창을 만드는 것을 먼저 진행하도록 한다.  스크롤이 되는 채팅창은 ListView 컴포넌트를 사용해서 구현하는데, 아래 그림과 같이 지난 메세지는 화면에 나오지 않고 현재 대화되는 상황만 보이도록 한다. 





리스트뷰(ListView) 에 대해서


그렇다면 리스트뷰는 무엇인가? 리스트뷰는 안드로이드 뷰 그룹 (View Group)의 일종으로, 스크롤이 가능한 아이템들의 리스트를 출력해주는 그룹이다.



아답터(Adaptor) 의 개념


채팅용 리스트뷰를 만들기 위해서는 아답터(Adaptor)의 개념을 이해해야 하는데, 아답터는 크게 두가지 기능을 한다. 리스트뷰에서 보여질 아이템들을 저장하는 데이타 저장소의 역할과, 리스트뷰안에 아이템이 그려질때 이를 렌더링하는 역할을 한다.


add 메서드를 이용하여, 아이템을 추가하고

getItem(int index)를 이용하여, index  번째의 아이템을 리턴하며 (자바의 일반적인 List형과 유사하다)

View getView(int position, xxx )이 중요한데, position 번째의 아이템을 화면에 출력할때 렌더링하는 역할을 한다.


그러면 실제로 작동하는 코드를 만들어보자. 이 예제에서는 텍스트를 입력하면 리스트 뷰에 추가되고, 텍스트가 입력됨에 따라 쭈욱 아래로 리스트뷰가 자동으로 스크롤되는 예제이다. 





아답터 클래스 구현


제일 먼저 채팅 메세지 리스트를 저장할 아답터 클래스를 구현해보자


package com.example.terry.simplelistview;


import android.content.Context;

import android.graphics.Color;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ArrayAdapter;

import android.widget.TextView;


import java.util.ArrayList;

import java.util.List;


/**

 * Created by terry on 2015. 10. 7..

 */

public class ChatMessageAdapter extends ArrayAdapter {


    List msgs = new ArrayList();


    public ChatMessageAdapter(Context context, int textViewResourceId) {

        super(context, textViewResourceId);

    }


    //@Override

    public void add(ChatMessage object){

        msgs.add(object);

        super.add(object);

    }


    @Override

    public int getCount() {

        return msgs.size();

    }


    @Override

    public ChatMessage getItem(int index) {

        return (ChatMessage) msgs.get(index);

    }


    @Override

    public View getView(int position, View convertView, ViewGroup parent) {

        View row = convertView;

        if (row == null) {

            // inflator를 생성하여, chatting_message.xml을 읽어서 View객체로 생성한다.

            LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            row = inflater.inflate(R.layout.chatting_message, parent, false);

        }


        // Array List에 들어 있는 채팅 문자열을 읽어

        ChatMessage msg = (ChatMessage) msgs.get(position);


        // Inflater를 이용해서 생성한 View에, ChatMessage를 삽입한다.

        TextView msgText = (TextView) row.findViewById(R.id.chatmessage);

        msgText.setText(msg.getMessage());

        msgText.setTextColor(Color.parseColor("#000000"));


        return row;


    }

}


add나 getItem,getCount등은 메세지를 Java List에 저장하고, n 번째 메세지를 리턴하거나 전체 크기를 저장하는 방식으로 구현한다.


가장 중요한 부분은 getView 메서드인데,리스트의 n 번째 아이템에 대한 내용을 화면에 출력하는 역할을 하며 여기서는 두 단계를 거쳐서 렌더링을 진행한다.


첫번째로는 인플레이터 (inflator)를 사용하여, 아이템을 렌더링할 View 컴포넌트를 ~/layout/XX.XML 에서 읽어오는 역할을 한다.

인플레이터에 대해서 간략하게 짚고 넘어가면, View등 화면등을 디자인할 때 안드로이드에서는 XML 을 사용하여 쉽게 View를 정의할 수 있다. 이렇게 정의된 XML을 실제 View 자바 Object로 생성을 해주는 것이 인플레이터(inflator)이다. 


    LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 


를 통해서 인플레이터를 생성하고, 이 인플레이터를 통해서 각 아이템을 출력해줄 View를 생성하여 row라는 변수에 저장한다.


 row = inflater.inflate(R.layout.chatting_message, parent, false);


이때, 레이아웃을 정의한 XML 파일은 chatting_message.xml 로 안드로이드 프로젝트의 ~/layout 디렉토리 아래에 있으며, 인플레이트 할때는 R.layout.chatting_message라는 이름으로 지칭한다.


chatting_message.xml 의 내용은 다음과 같다.


<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical" android:layout_width="match_parent"

    android:layout_height="match_parent">


    <TextView

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:textAppearance="?android:attr/textAppearanceMedium"

        android:text="Chat message"

        android:id="@+id/chatmessage"

        android:gravity="left|center_vertical|center_horizontal"

        android:layout_marginLeft="20dp" />

</LinearLayout>



이렇게 아이템을 표시할 View가 생성되었으면, 그 안에 알맹이를 채워넣어야 하는데, 채팅 메세지를 저장하는 List 객체에서, position 번째의 채팅 메세지를 읽어온 후에, row 뷰 안에 있는 TextView에 그 채팅 메세지를 채워 넣는다. 


getView 코드에서 주의해서 봐야할 부분이


  View row = convertView;

        if (row == null) { …


인데, 가만히 보면 row가 null 일 경우에만 인플레이터를 이용해서 row를 생성하는 것을 볼 수 있다.


ListView의 특징중 하나는, 아이템을 랜더링 하는 View 객체가 매번 생성되는 것이 아니라, 해당 아이템에 대해서 이미 생성되어 있는 View가 있다면, getView( .. ,View convertView, ..) 를 통해서 인자로 전달된다.


그래서, convertView가 null 인지를 체크하고, 만약에 null이 아닌 경우에만 View를 생성한다. 


여기까지가 채팅 메세지를 저장하고, 각 메세지를 렌더링 해주는 ListView 용 아답터의 구현이었다. 

그러면 아답터를 이용한 ListView를 사용하여 채팅 메세지를 출력하는 부분을 구현해보자


아래는 ListView를 안고 있는 MainActivity이다.


package com.example.terry.simplelistview;


import android.database.DataSetObserver;

import android.support.v7.app.ActionBarActivity;

import android.os.Bundle;

import android.view.Menu;

import android.view.MenuItem;

import android.view.View;

import android.widget.AbsListView;

import android.widget.EditText;

import android.widget.ListView;



public class MainActivity extends ActionBarActivity {

    ChatMessageAdapter chatMessageAdapter;


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }


    @Override

    public boolean onCreateOptionsMenu(Menu menu) {


        // Inflate the menu; this adds items to the action bar if it is present.

        getMenuInflater().inflate(R.menu.menu_main, menu);


        chatMessageAdapter = new ChatMessageAdapter(this.getApplicationContext(),R.layout.chatting_message);

        final ListView listView = (ListView)findViewById(R.id.listView);

        listView.setAdapter(chatMessageAdapter);

        listView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); // 이게 필수


        // When message is added, it makes listview to scroll last message

        chatMessageAdapter.registerDataSetObserver(new DataSetObserver() {

            @Override

            public void onChanged() {

                super.onChanged();

                listView.setSelection(chatMessageAdapter.getCount()-1);

            }

        });

        return true;

    }


    @Override

    public boolean onOptionsItemSelected(MenuItem item) {

        // Handle action bar item clicks here. The action bar will

        // automatically handle clicks on the Home/Up button, so long

        // as you specify a parent activity in AndroidManifest.xml.

        int id = item.getItemId();


        //noinspection SimplifiableIfStatement

        if (id == R.id.action_settings) {

            return true;

        }


        return super.onOptionsItemSelected(item);

    }


    public void send(View view){

        EditText etMsg = (EditText)findViewById(R.id.etMessage);

        String strMsg = (String)etMsg.getText().toString();

        chatMessageAdapter.add(new ChatMessage(strMsg));

    }

}


먼저 send는 “SEND” 버튼을 눌렀을때, 화면상에서 채팅 메세지를 읽어드려서 Adapter에 저장하는 역할을 한다.

가장 중요한 메서드는  onCreateOptionsMenu인데, 이 메서드의 주요 내용은, Adapter를 생성하여, Adapter를 listView에 바인딩한다.


  chatMessageAdapter = new ChatMessageAdapter(this.getApplicationContext(),R.layout.chatting_message);

        final ListView listView = (ListView)findViewById(R.id.listView);

        listView.setAdapter(chatMessageAdapter);


다음 기능으로는, 새로운 아이템이 listView에 추가되었을때, 맨 아래로 화면을 스크롤 하는 기능인데, 이는 Adapter에 DataObserver를 바인딩해서, 데이타가 바뀌었을때 즉 채팅 메세지가 추가되었을때, listView에서 리스트업 되는 아이템 목록을 맨 아래로 이동 시킨다.


        // When message is added, it makes listview to scroll last message

        chatMessageAdapter.registerDataSetObserver(new DataSetObserver() {

            @Override

            public void onChanged() {

                super.onChanged();

                listView.setSelection(chatMessageAdapter.getCount()-1);

            }

        });


이렇게 DataObserver를 추가하더라도, 아래로 스크롤이 안되는데, 새로운 아이템이 리스트뷰에 추가되었을 때 스크롤을 하게 해줄려면 TranscriptMode에 새로운 아이템이 추가되었을때 스크롤이 되도록 설정을 해줘야 한다. (디폴트는 새로운 아이템이 추가되더라도 스크롤이 되지 않는 옵션으로 설정되어 있다.)


listView.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL); // 이게 필수



간단하게, ListView를 이용한 채팅 UI를 만들어봤다. 다음에는 나인패치 이미지를 이용하여, 말풍선을 넣는 기능을 추가해보도록 한다.


참고 : http://javapapers.com/android/android-chat-bubble/



빠르게 훝어보는 node.js

#12 - Socket.IO (4/4)

조대협 (http://bcho.tistory.com)


채팅 프로그램에 (room/그룹) 기능을 추가하기

다음은 앞에서 만든 1:1 귓속말이 가능한 채팅에 채팅방기능을 추가한 버전이다.

var express = require('express');

var routes = require('./routes');

var http = require('http');

var path = require('path');

 

var app = express();

app.use(express.bodyParser());

app.use(express.cookieParser('your secret here'));

app.use(express.session());

app.use(express.static(path.join(__dirname, 'public')));

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'ejs');

app.use(express.favicon());

app.use(express.logger('dev'));

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

 

var httpServer =http.createServer(app).listen(3000, function(req,res){

    console.log('Socket IO server has been started');

});

// upgrade http server to socket.io server

var io = require('socket.io').listen(httpServer);

 

var count = 0;

var rooms = [];

 

app.get('/:room',function(req,res){

    console.log('room name is :'+req.params.room);

    res.render('index',{room:req.params.room});

});

 

 

 

io.sockets.on('connection',function(socket){

 

    socket.on('joinroom',function(data){

        socket.join(data.room);

 

        socket.set('room', data.room,function() {

            var room = data.room;

            var nickname = '손님-'+count;

            socket.set('nickname',nickname,function(){

                socket.emit('changename', {nickname: nickname});

 

                // Create Room

                if (rooms[room] == undefined) {

                    console.log('room create :' + room);

                    rooms[room] = new Object();

                    rooms[room].socket_ids = new Object();

                }

                // Store current user's nickname and socket.id to MAP

                rooms[room].socket_ids[nickname] = socket.id

 

                // broad cast join message

                data = {msg: nickname + ' 님이 입장하셨습니다.'};

                io.sockets.in(room).emit('broadcast_msg', data);

 

                // broadcast changed user list in the room

                io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                count++;

            });

        });

 

    });

 

    socket.on('changename',function(data){

        socket.get('room',function(err,room){

            socket.get('nickname',function(err,pre_nick) {

                var nickname = data.nickname;

                // if user changes name get previous nickname from nicknames MAP

                if (pre_nick != undefined) {

                    delete rooms[room].socket_ids[pre_nick];

                }

                rooms[room].socket_ids[nickname] = socket.id

                socket.set('nickname',nickname,function() {

                    data = {msg: pre_nick + ' 님이 ' + nickname + '으로 대화명을 변경하셨습니다.'};

                    io.sockets.in(room).emit('broadcast_msg', data);

 

                    // send changed user nickname lists to clients

                    io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                });

            });

 

        });

    });

 

 

    socket.on('disconnect',function(data){

        socket.get('room',function(err,room) {

            if(err) throw err;

            if(room != undefined

                && rooms[room] != undefined){

 

                socket.get('nickname',function(err,nickname) {

                    console.log('nickname ' + nickname + ' has been disconnected');

                    // 여기에 방을 나갔다는 메세지를 broad cast 하기

                    if (nickname != undefined) {

                        if (rooms[room].socket_ids != undefined

                            && rooms[room].socket_ids[nickname] != undefined)

                            delete rooms[room].socket_ids[nickname];

                    }// if

                    data = {msg: nickname + ' 님이 나가셨습니다.'};

 

                    io.sockets.in(room).emit('broadcast_msg', data);

                    io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                });

            }

        }); //get

    });

 

    socket.on('send_msg',function(data){

        socket.get('room',function(err,room) {

            socket.get('nickname',function(err,nickname) {

                console.log('in send msg room is ' + room);

                data.msg = nickname + ' : ' + data.msg;

                if (data.to == 'ALL') socket.broadcast.to(room).emit('broadcast_msg', data); // 자신을 제외하고 다른 클라이언트에게 보냄

                else {

                    // 귓속말

                    socket_id = rooms[room].socket_ids[data.to];

                    if (socket_id != undefined) {

 

                        data.msg = '귓속말 :' + data.msg;

                        io.sockets.socket(socket_id).emit('broadcast_msg', data);

                    }// if

                }

                socket.emit('broadcast_msg', data);

            });

        });

    })

});

코드를 살펴보자

처음에 입장은 http://localhos:3000/{방이름} 으로 하게 된다.

app.get('/:room',function(req,res){

    console.log('room name is :'+req.params.room);

    res.render('index',{room:req.params.room});

});

그러면 URL에 있는 방이름을 받아서, index.ejs에 있는 UI로 채팅창을 띄워주고 방이름을 parameterindex.ejs에 넘겨준다.

 

    socket.on('joinroom',function(data){

        socket.join(data.room);

클라이언트가 서버에 접속되면 먼저 클라이언트가 join 이벤트를 보내는데, join 이벤트를 받으면, 이때 같이 room 이름으로 현재 소켓을 room 이름의 room join한다.

        socket.set('room', data.room,function() {

다음으로, 해당 소켓이 어느 룸에 있는지 set 명령을 이용하여 socket 저장해놓는다.

            var room = data.room;

            var nickname = '손님-'+count;

            socket.set('nickname',nickname,function(){

                socket.emit('changename', {nickname: nickname});

 

                // Create Room

                if (rooms[room] == undefined) {

                    console.log('room create :' + room);

                    rooms[room] = new Object();

                    rooms[room].socket_ids = new Object();

                }

윗부분이 room 데이터 객체를 생성하는 것인데, 앞의 예제와는 달리, 현재 연결된 클라이언트의 socket.id 이제는 room 단위로 관리를 해야 한다. 그래서 rooms라는 객체를 이용하여, 해당 room 대해서 rooms.room이라는 객체로 만들고, 그리고, room 현재 연결된 클라이언트 socket.id 저장하는 socket_ids 객체를 생성한다.

                // Store current user's nickname and socket.id to MAP

                rooms[room].socket_ids[nickname] = socket.id

 

그리고 나서, socket_ids 귓속말 채팅방 예제와 같이 nickname to socket.id 대한 맵핑 정보를 저장한다.

                // broad cast join message

                data = {msg: nickname + ' 님이 입장하셨습니다.'};

                io.sockets.in(room).emit('broadcast_msg', data);

// broadcast changed user list in the room

                io.sockets.in(room).emit('userlist', {users:

Object.keys(rooms[room].socket_ids)});

                count++;

            });

그리고 위와 같이 현재 room 들어 있는 클라이언트들에게만 , 새로운 사용자가 입장했음을 알리고, 사용자 리스트를 업데이트하는 이벤트를 보낸다.

disconnect 대한 부분도 크게 달라진 것이 없다. Socket_ids 객체가 rooms 아래로 들어갔고, 메시지를 보낼때, 귓속말 채팅방 예제가 io.sockets.emit 대신에, room 범위를 지정하는 in() 메서드를 써서 io.sockets.in(room).emit 같이 보내게 된다.

Sendmsg 이벤트 부분도, broadcast하는 부분에서 to를 이용하여 다음과 같이socket.broadcast.to(room).emit 특정 room에 있는 클라이언트에게만 메시지를 보내는 것으로만 변경되었다

아래는 클라이언트쪽의 코드이다. 앞의 예제에서 나온 귓속말이 가능한 대화방과 코드가 거의 동일하다.

<script type="text/javascript">

        var socket = io.connect('http://localhost');

        socket.emit('joinroom',{room:'<%=room%>'});

처음에 접속하였을 때, 서버 코드에서 방이름을 URL로부터 읽어서, 그 방이름으로 join 하는 이벤트를 보낸다.

/vies/index.ejs

<html>

<head>

 

    <title></title>

    <script src="/socket.io/socket.io.js"></script>

    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>

 

</head>

<body>

 

<b>Welcome ROOM : <%= room%></b><p>

    Name <input type="text" id="nickname" /> <input type="button" id="changename" value="Change name"/><br>

    To

    <select id="to">

        <option value="ALL">ALL</option>

    </select>

    Message  <input type="text" id="msgbox"/>

    <br>

    <span id="msgs"></span>

 

    <script type="text/javascript">

        var socket = io.connect('http://localhost');

        socket.emit('joinroom',{room:'<%=room%>'});

 

        $('#changename').click(function(){

            socket.emit('changename',{nickname:$('#nickname').val()});

        });

        $("#msgbox").keyup(function(event) {

            if (event.which == 13) {

                socket.emit('send_msg',{to:$('#to').val(),msg:$('#msgbox').val()});

                $('#msgbox').val('');

            }

        });

        socket.on('new',function(data){

            console.log(data.nickname);

            $('#nickname').val(data.nickname);

        });

 

        // 새로운 사용자가 들어오거나, 사용자가 이름을 바꿨을때 "To" 리스트를 변경함

        socket.on('userlist',function(data){

            var users = data.users;

            console.log(users);

            console.log(data.users.length);

            $('#to').empty().append('<option value="ALL">ALL</option>');

            for(var i=0;i<data.users.length;i++){

                $('#to').append('<option value="'+users[i]+'">'+users[i]+"</option>");

            }

        });

 

        socket.on('broadcast_msg',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

        });

    </script>

</body>

</html>

 

다음은 실제 실행 화면이다.



빠르게 훝어보는 node.js

#10 - Socket.IO (2/4)

조대협 (http://bcho.tistory.com)


Socket.IO APIs

Socket.IO 이밖에도 다양한 이벤트를 전달할 있는 API 제공하는데, 이에 대해서 살펴보자.

여기서 사용하는 socket이라는 객체는

io.sockets.on('connection',function(socket){

의해서 callback function 의해서 전달된 인자임을 미리 명시해둔다.


1. 이벤트 보내기 받기

먼저 소켓으로 또는부터 이벤트를 보내고 받는 방법부터 알아보자.앞에 예제에서도 봤지만 가장 간단한 방법은

Ÿ  * 이벤트 보내기 socket.emit('이벤트명',{메세지});

현재 연결되어 있는 클라이언트 소켓에 “이벤트명”으로 “{메시지}” 데이터로 이벤트를 보낸다.

Ÿ  *  이벤트 받기 socket.on('이벤트명',function(data){ });

현재 연결되어 있는 클라이언트 소켓으로부터 들어오는 이벤트명이벤트에 대해서 두번째 인자로 정의된 callback function 의해서 이벤트에 대한 처리를 한다. 이때 이벤트 메시지는 callback function 인자인 “data” 통해서 전달된다.

하나의 클라이언트가 아니라 다수의 다른 클라이언트나 또는 다른 클라이언트에 이벤트를 어떻게 보내는지 알아보자

Ÿ  * 나를 제외한 다른 클라이언트들에게 이벤트 보내기 socket.broadcast.emit('이벤트명',{메세지});

socket 대해서 broadcast 하면, 나를 제외한 다른 소켓 클라이언트들에게 이벤트를 보낼 있다.

Ÿ 나를 포함한 모든 클라이언트들에게 이벤트 보내기io.sockets.emit('이벤트명',function(data){ });

개별 클라이언트 소켓을 대표하는 객체가 socket이라면, 전체 연결된 socket들을 대표하는 객체는io.sockets이다. 여기서는 io.sockets.emit 사용했는데, 이는 전체 연결된 클라이언트 소켓에 대해서 이벤트를 보내도록 것이다.

소켓이 아닌 다른 특정 소켓에게 이벤트를 보내는 방법이 있는데,

Ÿ  * io.sockets(socket_id).emit('이벤트명',function(data){ });

사용하면 된다. 이때 socket_id socket.id 값으로 클라이언트 소켓은 id라는 property 가지고 있고, 이는 소켓을 구별해주는 식별자가 된다. 그나중에 예제에서 설명하겠지만, 채팅 귓속말과 같이 특정 소켓으로 메시지를 보내려면, 메시지를 전달하는 대상이 되는 소켓의 id값을 알아서 위의 io.socket(socket_id) 이벤트를 전달해야 한다.


2.  소켓에 데이터 바인딩

소켓에는 소켓에 연관된 데이터를 set 메서드를 이용해서 binding하고, get 이용해서 binding 데이터를 꺼낼 있다.

Ÿ   * socket.set(‘key’, ‘value’,function() {});

Ÿ   socket.get(‘key’, function(err,value) {});

Ÿ   socket.del(‘key’, function(err,value) {});

값으 저장할때는 set을 이용하여 socket key값을 키로 사용하여 value라는 데이터를 저장한다. 값을 읽어올 때는 get을 이용하여 socket에 저장된 key이름으로 저장된 값을 value를 통해서 리턴한다.그리고 해당값을 삭제하고자 할때는 del을 이용하여 socket key이름으로 저장된 값을 삭제한다.

socket도 객체이기 때문에 Object property를 사용해도 되는데, 예를 들어 socket.key = value 왜 굳이 set/get을 사용할까? socket.ioset/get 내부 구현을 뜯어보면 실제로는 Object property를 사용한다. socket.iostore Redis로 지정하게 되면, set/get은 값을 내부 Object property에 저장하지 않고, Redis에 저장하게 되서, 이 값들은 클러스터 노드 (다른 노드간)에서도 접근이 가능해진다.

3.  Room 처리 (그룹)

socket.io 소켓들을 그룹핑하는 채널과 같은 개념인 ‘room’이라는 개념을 지원한다. 채팅 프로그램의 대화방과 같은 개념이다. Room 사용하게 되면 broadcast 하더라도 같은 room안에 있는 클라이언트에게만 이벤트가 전송된다.

Ÿ   socket.join(‘roon name’)

Ÿ   socket.leave(‘roon name’)

소켓을 특정 room binding하는 방법은 join 이용하면 해당 소켓은 room binding 되고, leave 하면 room에서부터 나오게 된다.

특정 room 있는 socket에게 이벤트를 보내는 것은 앞에 설명한 똑같이 socket.emit 사용하면 되는데, broadcast 하거나 room안에 있는 전체 클라이언트 소켓에게 이벤트를 보낼때는 아래와 같이 room 명시해주면 된다

Ÿ   io.sockets.in(‘roon name’).emit(‘event’,message)

‘room name’ room안에 있는 모든 클라이언트들에게 이벤트 보내기

Ÿ   socket.broadcast.to(‘roon name’).emit(‘event’,message)

 ‘room name’ room 안에 있는 나를 제외한 다른 클라이언트들에게 이벤트 보내기

또한 현재 생성되어 있는 room 대한 정보를 읽어오는 방법이 몇가지가 있는데,

Ÿ  io.sockets.manager.rooms

현재 생성되어 있는 모든 room 목록을 리턴한다.

Ÿ  io.sockets.clients(‘roon name’)

 ‘room name’ room 안에 있는 모든 클라이언트 소켓 목록을 리턴한다.

몇가지 주요 메서드들을 성명하였지만, 여기서 설명한 것은 일부에 불과하다. 상세한 내용들은 아래 내용을 참고하기 바란다.

상세한 Configuration 처리 https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO

이벤트 종류(disconnect ) https://github.com/LearnBoost/socket.io/wiki/Exposed-events

자아 그러면 소개된 메서드들을 가지고, 앞에서 만든 채팅 프로그램의 기능을 더해보도록 하자

빠르게 훝어보는 node.js

#9 - Socket.IO (1/4)

조대협 (http://bcho.tistory.com)


웹의 발전과 함께, 클라이언트의 요청에 대해서 응답만을 하는 단방향성이 아닌 양방향성의 사이트가 유행하게 되었는데, Socket.IO 자바스크립트 모듈로, 양방향 통신이 가능한 웹사이트를 구축하기 위해서 HTTP 클라이언트로 푸쉬 메시지를 보내줄 있는 모듈이다. 넓은 브라우져 지원성과 사용의 편의성 때문에 널리 사용되고 있고, node.js 인기 있어 지는 이유 중의 하나는 socket.io 때문이 아닐까 한다.

배경

Socket.io를 설명하기 전에, 웹에서의 푸쉬 개념에 대해서 이해할 필요가 있다. 웹은 기반적으로 클라이언트에서 서버로 가는 단방향성이지만, 채팅과 같은 실시간 양방향 애플리케이나 쪽지와 같이 서버에서 클라이언트로 알림을 보내줘야 하는 요구 사항이 생겼다. 그래서 여러가지 기법이 생겨났는데, 자바스크립트 기반의 AJAX가 유행하면서 몇가지 기법이 생겨났는데, 그 내용을 살펴보면 다음과 같다.



웹 푸쉬 방식 비교 : Polling vs Long Polling vs Streaming

출처 : https://blogs.oracle.com/theaquarium/entry/slideshow_example_using_comet_dojo


Polling

가장 기본적인 기법으로, 클라이언트가 서버에 주기적으로 폴링(request를 보내는 기법)이다. 주기적으로 클라이언트가 자기가 처리해야할 이벤트가 있는지 없는지를 체크하는 것이다.

서버가 폴링 요청이 들어올때 마다 이를 처리해야 하고, 다음 폴링이 이루어지기 전까지는 어떤 이벤트가 오는지를 모르기 때문에, 결정적으로 실시간성이 보장이안된다.

예를 들어 폴링 주기가 10분이라고 할때 폴링 이후에 바로 이벤트가 들어왔을때, 다음 폴링주기 (10)을 기다려야 된다. 그리고 폴링 주기가 짧을 수 록 서버가 받는 부하가 크다, 예를 클라이언트가 작업할 내용이 있는지 확인하려면, DB드에 작업 내용을 저장해놓고, 폴링 때마다 체크해야 하는데, 이때 매번 DB 쿼리를 해야 한다면, 폴링 때마다 서버가 DB를 쿼리해야하기 때문에, 받아야 하는 트렌젝션이 매우 많고 서버의 부담이 기하 급수적으로 늘어난다. 따라서, 짧은 폴링 주기는 서버에 많은 부하를 주기 때문에, 적절하지 않으며 클라이언트로 보내는 푸쉬 메시지의 실시간성이 필요하지 않은 경우에 적절하며, 서버의 부하가 상대적으로 적고(폴링 주기가 길 경우) 기존의 웹백엔드 인프라 (Tomcat과 같은 미들웨어)를 그대로 활용할 수 있는 장점을 가지고 있다.


Long Polling

Long Polling Polling과 비슷하나 즉시성을 갖는다. 방식은 클라이언트가 HTTP request를 보내고, 바로 request를 닫는 것이 아니라, 일정 시간 동안(오랫동안) 열어 놓고 있다가 서버에서 클라이언트로 보내는 메시지가 있으면 메시지를 HTTP response로 실어 보내고, 해당 Connection을 끊는다. 만약에 일정 시간동안 보낼 메시지가 없으면 HTTP 연결을 끊는다.

응답 메시지를 받건 안받건, 끊어진 연결은 다시 연결한다. 기본적으로 클라이언트가 연결을 해서 응답을 요청하는 Polling 형태이고, 응답이 오는지 기다리는 기간이 길기 때문에 이를 Long Polling 이라고 한다.

Long Polling의 경우 서버에 클라이언트들이 거의 항상 연결되어 있는 형태이기 때문에, 동시  서버의 동시에 연결할 수 있는 서버가 지원할 수 있는 동시 연결(Connection)수에 따라 결정된다.

예를 들어 Tomcat과 같은 WAS의 경우에는 HTTP 연결이 열려 있는 경우에는 1개의 Thread가 그 요청을 처리하기 위해서 사용되기 때문에, Tomcat Thread 100개인 경우, 1 Tomcat당 처리할 수 있는 Long Polling 가능한 클라이언트 수는 100개로 한정이 된다. (기존의 HTTP 요청을 처리하는 인프라로 핸들링하기 어렵다.)

서버로부터 푸쉬 메시지를 받으면 재 연결을 해야 하기 때문에, 클라이언트로 푸쉬하는 내용이 적을 경우에 유리하며 실시간 채팅과 같이 푸쉬해야 하는 메시지가 많은 경우에는 적절하지 않다. (채팅 메시지가 하나 왔다갔다 할 때 마다 재 연결을 해야 한다.)


Streaming

마지막으로 Streaming 기법인데, 이 기법은 일반적인 TCP Connection 처럼, 클라이언트가 서버로 연결을 맺은 후에, 그 연결을 통해서 서버가 이벤트를 보내는 방식이다. Long Polling이 이벤트를 받을 때마다 연결을 끊고 재 연결을 한가면, Streaming 방식은 한번 연결되면 계속해서 그 연결을 통해서 이벤트 메시지를 보내는 방식으로 재연결에 대한 부하가 없다.


WebSocket

이러한 푸쉬 로직을 AJAX 자바스크립트로 구현하다가 구현 방식이 브라우져들 마다 각기 상이하기 때문에 나온 표준이 WebSocket이라는 표준이다. http:// 대신 ws:// 로 시작하며 Streaming과 유사한 방식으로 푸쉬를 지원한다.

그러나 문제는 이 WebSocket 기술이 근래에 나왔기 때문에, 예전 브라우져는 지원하지 않는다는 것이다.



<그림: 웹소켓 지원 브라우져 현황>

출처 : http://caniuse.com

Socket.IO

Socket.IO 클라이언트로의 푸쉬를 지원하는 모듈인데, WebSocket 한계를 뛰어 넘어주는 모듈이다. 개발자는 Socket.IO 개발을 하고 클라이언트로 푸쉬 메시지를 보내기만 하면, WebSocket 지원하지 않는 브라우져의 경우, 브라우져 모델과 버전에 따라서 AJAX Long Polling, MultiPart Streaming, Iframe 이용한 푸쉬, JSONP Polling, Flash Socket 다양한 방법으로 내부적으로 푸쉬 메시지를 보내준다.

WebSocket 지원하지 않는 어느 브라우져라도 푸쉬 메시지를 일관된 모듈로 보낼 있다.

간단한 예제를 살펴보자


간단한 채팅 프로그램

간단한 채팅 프로그램을 하나 살펴보자. 아래 프로그램은 브라우져에 접속하면 메시지를 입력 받을 있는 Input Box 띄워주고, 여기에 메시지를 입력하면 현재 연결되어 있는 모든 웹브라우져에 메시지를 보내주는 프로그램이다.

먼저 서버쪽 코드를 보자

/**

 * Module dependencies.

 */

 

var express = require('express');

var routes = require('./routes');

var http = require('http');

var path = require('path');

 

var app = express();

app.use(express.static(path.join(__dirname, 'public')));

 

var httpServer =http.createServer(app).listen(8080, function(req,res){

  console.log('Socket IO server has been started');

});

// upgrade http server to socket.io server

var io = require('socket.io').listen(httpServer);

 

io.sockets.on('connection',function(socket){

   socket.emit('toclient',{msg:'Welcome !'});

   socket.on('fromclient',function(data){

       socket.broadcast.emit('toclient',data); // 자신을 제외하고 다른 클라이언트에게 보냄

       socket.emit('toclient',data); // 해당 클라이언트에게만 보냄. 다른 클라이언트에 보낼려면?

       console.log('Message from client :'+data.msg);

   })

});

 

<코드. App.js>

Express에서 Socket IO 사용한 예제인데, 기존에 Express에서 사용한것과 같은 방식으로 httpServer 생성한다. 다음에, httpServer socketIO 지원하는 서버로 다음과 같이 업그레이드를 한다.

var io = require('socket.io').listen(httpServer);

다음으로, 클라이언트가 socket.io 채널로 접속이 되었을때에 대한 이벤트를 정의한다.

io.sockets.on('connection',function(socket){

같이 클라이언트가 접속이 되면, callback 수행하는데, 이때, 연결된 클라이언트의 socket 객체를 같이 넘긴다. socket 객체를 받아서, 코드에서는 연결된 클라이언트에게 “Welcome !”이라는 메시지를 보냈다.

socket.emit('toclient',{msg:'Welcome !'});

일반적인 이벤트 처리 방식과 같게, 해당 클라이언트 소켓에 emit 메서드를 이용하여, 이벤트를 전송하면 된다. 여기서는 “toclient”라는 이벤트 명으로 msg라는 키를 갖고, value ‘Welcome !’ 이라는 값을 가지는 메시지를 전송하였다.

다음으로, 클라이언트로부터 오는 메시지를 처리하는 루틴인데, 채팅창에서 글을 쓰고 엔터를 누르면 서버로 “fromclient” 라는 이벤트를 보내도록 작성해놓았다. 그러면 서버쪽에서는 다음과 같이 socket.on(‘fromclient’ 라는 메서드를 이용하여 해당 이벤트에 따른 처리를 한다. 이때 들어오는 데이터는 채팅 문자열이 {msg:”문자열”} 형식으로 data라는 변수를 통해서 아래와 같이 들어오는데,

socket.on('fromclient',function(data){

채팅에서 이메세지를 다른 클라이언트들과 자신에게 다시 보낸다.

       socket.broadcast.emit('toclient',data);

       socket.emit('toclient',data);

socket.broadcast.emit 자신을 제외한 다른 모든 클라이언트에게 이벤트를 보내는 메서드이고, socket.emit 자신의 클라이언트()에게 이벤트를 보내는 메서드이다.

그러면 이제 클라이언트()쪽의 코드를 보자

<html>

<head>

 

    <title></title>

    <script src="/socket.io/socket.io.js"></script>

    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>

 

</head>

<body>

    <b>Send message</b><p>

    Message  <input type="text" id="msgbox"/>

    <br>

    <span id="msgs"></span>

 

    <script type="text/javascript">

        var socket = io.connect('http://localhost');

        $("#msgbox").keyup(function(event) {

            if (event.which == 13) {

                socket.emit('fromclient',{msg:$('#msgbox').val()});

                $('#msgbox').val('');

            }

        });

        socket.on('toclient',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

        });

    </script>

</body>

</html>

<코드. index.html>

먼저 socket.io 사용하기 위해서 script src 아래와 같이 정의하고

<script src="/socket.io/socket.io.js"></script>

다음으로, 자바 스크립트가 실행되면, socket.io 서버로 연결을 한다.

var socket = io.connect('http://localhost');

그리고, input box에서 엔터를 누르면, input box 메시지를 읽어서, ‘fromclient’라는 이벤트를 서버에 전송한다.

socket.emit('fromclient',{msg:$('#msgbox').val()});

그리고 반대로, 서버로부터, ‘toclient’라는 이벤트가 들어오면, 들어온 문자열을 msgs라는 id 갖는 <span> 영역에 append 한다.

socket.on('toclient',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

클라이언트와 서버쪽 코드가 완성되었으면, 이를 배포하고 node.js 실행해서 테스트를 해보자.



<그림. socket.io 이용한 채팅 프로그램 화면>

다음에는 Socket.IO API들에 대한 소개와 1:1 귓속말, 그리고 그룹의 개념을 가지는 채팅방 예제에 대해서 설명하도록 한다.