참고 :

http://sd.wareonearth.com/~phil/jumbo.html

http://windy.luru.net/565


10 기가비트 네트워크 카드, 10 기가 지원 스위치(가격이 좀 비싸다.) 가 있는 상황에서 

10 기가비트 네트워크 환경에서 제속도를 내기 위해서는 아래의 3가지 사항이 중요하다.


1. switch jumbo frame 설정
   switch의 jumbo frame(packet)을 지원하는 switch 라면 설정을 enable 시키고 만약 사이즈도 지정해야 한다면
   9014 Bytes 로 지정해라.

2. Network Interface Card MTU 설정
   nic 의 MTU 값은 기본 1500 이다. 이 값을 9000 으로 바꾸어라. 테스트 결과 1500일 경우 10Gigabit 의 절반정도의 효율만 보엿다.
   리눅스의 경우 ifconfig eth mtu 9000 입력하면 일시적으로 바뀐다.

3. 위의 2가지 사항 말고도 실제적인 속도 향상을 위해서는 장비의 메모리와 cpu 사양이 높아야한다.
   특히 패킷을 받는 장비의 경우 메모리가 낮을 경우 네트워크의 최대 속도를 장비가 따라가지 못할 수 있다.




리눅스에서 네트워크 속도 테스트의 경우 

netcat , iperf 로 확인하였다. 자세한 사용법은 구글링 하시길..

iperf 는 이론상의 네트워크 대역폭에 근접하게 뜨고 netcat 는 장비의 메모리나 cpu 등등 환경에 의한 실제적인 네트워크 대역폭이 나온다. 필자의 경우 netcat으로 테스트하고 속도가 이론상보다 낮게 나왔을때 메모리나 cpu, 네트워크 설정 등을 확인하였다.


netcat 간단 사용법

server : nc -v -v -l -n 2222 > /dev/zero
client : time dd if=/dev/zero bs=1M count={1000 or 10000} |  nc -v -v -n {server-ip} 2222


iperf 간단 사용법

server : iperf -s
client : iperf -c {server-ip}





IP 할당 규칙

기본적인 규칙은 아래와 같다.

XXX.{vlan-number}.{subsystem-number}.{node-index+10}

l  XXX : 는 시작 주소로 특정할 걸 정한다. )100

l  {vlan-number} : 이것은 ip가 사용되는 vlan 번호이다.

l  {subsystem-number} : 경영팀 = 0 , 운영팀 = 1, 영업팀 = 2, 지원팀 = 3 , 기타 = 4 이다.

l  {node-index} : subsystem내 현재 장비의 번호에 10을 더한 값이다.
10
을 더하는 이유는 1~9번은 대개 게이트웨이나 기타 등등으로 쓰이기 때문에 제외했다.


netmask 255.255.248.0 혹은 255.255.252.0 이다.
255.255.248.0 같은 경우 xxx.xxx.0.0 ~ xxx.xxx.7.255 까지 가질수 있으며
255.255.252.0 같은 경우 xxx.xxx.0.0 ~ xxx.xxx.3.255 까지 가질수 있다.
 

예를 들어 XXX 100이라 정했고 vlan 번호는 20 이. 또한 이 장비는 영업팀에 있고 5번째로 추가된 장비일때 아이피 주소는 100.20.2.15 이다.


VLAN 에 대한 ip 할당을 체계적으로하고 VLAN에 대한 dhcp 서버 설정시 유용할 것 같아서 규칙을 만들어 보았다.

참고 :

 http://plucky.tistory.com/19
http://ryusstory.tistory.com/177
http://blog.naver.com/PostView.nhn?blogId=hhihealth&logNo=150164848875
http://helpboy.tistory.com/entry/%ED%8D%BC%EC%98%A8%EA%B8%80-VLAN-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC
http://jeongsam.net/38
http://cloudroot.blogspot.kr/2012/01/26-vlan.html
http://kthan.tistory.com/99
http://ko.wikipedia.org/wiki/%EA%B0%80%EC%83%81_%EB%9E%9C
http://en.wikipedia.org/wiki/VLAN_Trunking_Protocol
http://okay19.tistory.com/entry/VTP-VLAN-Trunking-Protocol
http://blog.daum.net/ksch2120/2
http://blog.naver.com/PostView.nhn?blogId=demonicws&logNo=40108781898&redirect=Dlog&widgetTypeCall=true
http://www.ccnpguide.com/end-to-end-vs-local-vlan-models/ - End-to-End vs. Local VLAN Models
https://wiki.ubuntu.com/vlan - Ubuntu vlan 설정
http://docs.fedoraproject.org/en-US/Fedora/17/html/System_Administrators_Guide/s2-networkscripts-interfaces_802.1q-vlan-tagging.html - Fedora vlan 설정
http://netgear.tonidoid.com/app/webshare/share/NETGEAR%20Switch%20Tech%20Documents/[Switch]%20LACP_%EC%84%A4%EC%A0%95%EB%B0%A9%EB%B2%95.pdf - LACP 구성방법


http://www.netmanias.com/ko/?m=view&id=techdocs&no=4814 - vlan 기본 개념(정리가 잘되어 있음)


목차
개념
장점
VLAN 구성 방법
VTP (VLAN Trunking Protocol)
 - Trunking(트렁킹)
 - Tagging
 - Trunk 의 필요성
 - VTP Mode(모드)
 - switchport mode 명령어의 Trunk 설정
 - VTP Prunig (프루닝)
VLAN 설정
 - linux VLAN 설정
VLAN 활용
VALN 간 통신





개념

Vlan 이란 Virtual Local Area Network 의 약자로 물리적 배치와 상관없이 논리적으로 LAN을 구성할 수 있는 기술이다.

예를 들자, 집에 2개의 스위치가 있고 2개의 스위치가 각각의 컴퓨터들을 연결했다고하자. 이 2개의 스위치는 서로 연결되지 않았다고하자. 그럼 2개의 스위치의 컴퓨터들은 서로 통신할 방법이 없다. 이것이 물리적 lan 환경이다. 물리적으로 서로를 떨어트려 독립적인 네트워크를 구성한 것이다.

VLAN 은 이런 물리적 네트워크 구성이 아닌 논리적 네트워크 구성을 가능케한다. 하나의 스위치가 있고 이 스위치에는 포트가 1~10 까지 있다고 하자. 1~5 를 VLAN 1 로 6~10을 VLAN 2 로 설정했다면 물리적으로 같은 스위치에 존재해도 VLAN 1과 VLAN 2는 서로 통신하지 못한다.

스위치의 모든 인터페이스는 동일 브로드캐스트 도메인에 포함되어 있으나 VLAN을 적용할 경우 스위치의 일부 인터페이스를 하나의 브로드캐스트 도메인으로 구성하고, 다른 인터페이스를 또다른 브로드캐스트 도메인으로 구성하여 여러 개의 도메인을 만들수 있습니다. 이렇게 스위치에 의해 논리적으로 만들어진 브로드 캐스트 도메인을 VLAN이라고 합니다.



장점

1 . 네트워크 리소스 보안을 높인다.

네트워크 그룹 설정을 변경하거나 이동하게 되면 보안상의 문제가 발생할 우려가 있지만 VLAN을 이용하면 실제적인 네트워크 그룹의 이동이 없어도 되어 보안상의 문제를 쉽게 줄일 수 있다.


2. 비용을 절감할 수 있다.

VLAN 기술을 쓰지 않는다면 서로 차단된 LAN 환경을 구축할 때 장비가 추가로 필요하게 된다. 하지만 VLAN을 이용한다면 장비의 추가 없이 차단된 LAN 환경을 구축할 수 있다.


3. 관리자의 네트워크 설정작업에 용이하다.

네트워크 관리자가 특정 장비의 네트워크 그룹을 옮겨야할 때 실 장비를 옮기는 과정없이 스위치 설정만으로 네트워크 그룹을 옮길수 있으므로 편하다.


4. 불필요한 트래픽을 줄인다.

VLAN 은 서로 다른 네트워크 그룹이기 때문에 브로드케스트 패킷이 다른 VLAN으로 전송되지 않습니다.

세분화하여 Broadcat domain 을 나눌 수 있기 때문에 불필요한 트래픽을 현격히 줄일수 있습니다.

예를 들어 IP 전화기의 트래픽과 PC의 트래픽을 분리합니다.



VLAN 구성 방법

1. PORT



스위치의 각 포트별로 VLAN을 할당하는 방법이다. 예를들어 FastEthernet 0/1,2,3 을 VLAN 10 , FastEthernet 0/4,5,6 을 VLAN  20 으로 각각의 포트에 대해 다른 VLAN을 설정 하는 것이다.

장비(PC와 같은 장비)에 어떠한 설정도 따로 해 줄 필요가 없고 VLAN을 구성하는 포트에 장비를 연결하면 그 장비는 해당 VLAN의 네트워크 그룹에 포함되는 방식이다.

설정은 쉽지만 장비에 대해서 VLAN을 설정하는 방법이 아니기 때문에 장비가 다른 포트에 연결된다면 해당 장비는 다른 VLAN 을 구성하게 된다. 




2. MAC address



모든 장비에는 고유의 MAC address 를 가진다. 스위치에 특정 MAC address에 대해 VLAN 을 구성하면 해당 장비가 어떤 포트에 연결되던 동일한 VLAN 구성을 갖는다. VLAN을 관리하는 관리자가 각 장비별로 MAC address 를 알고 있어야한다는 점에서 불편하다. NIC(Network Interface Card) 를 추가하거나 교환하거나 하는 상황에서 스위치의 MAC address 에 관한 VLAN 을 다시 구성해야한다는 점에서 불편할 수도 있다.





3. End-to-End VLANs



VLAN 이 전체 스위치(스위치간) 네트워크에 걸쳐져있는 것을 말한다.

만약 사용자가 이동하더라도 user VLAN 멤버쉽은 그대로 유지된다. end-to-end vlan 은 80%의 트래픽은 local workgroup안에서 이루어지고 20%만 remote resource를 목적지로 나가는 상황에서 좋다.

end-to-end VLAN은 설정하기 간편하고 편리한 장점이 있지만 모든 트래픽이 core-layer를 지나가게 되므로 Broadcast Storm 이나 L2 Bridging Loop 와 같은 이유로 CPUI의 리소스를 많이 사용하게되어 Enterprise Network 에서는 권장되지 않습니다.

사용자의 물리적 위치에 상관없이 동일한 VLAN에 묶일수 있습니다.





4. Local VLANs



VLAN 이 각 Access-layer 에 따라서 VLAN이 다르게 지정되는 것을 말합니다.

end-to-end VLAN과 반대로 사용자가 이동하면 다른 VLAN에 묶이게 됩니다.

20%의 트래픽이 Local에서 일어나고 80%의 트래픽이 Remote resource 에서 일어날 때 좋습니다. Local VLAN을 사용하면 처음 생성과 관리에는 불편하지만 장애 발생시 장애 요인을 찾기가 쉽습니다.







VTP (VLAN Trunking Protocol)

복수개의 스위치들이 VLAN 설정 정보를 교환할 때 사용하는 프로토콜



Trunking(트렁킹)

각기 다른 VLAN이 데이터를 주고 받을수 있게 하는 한 라인으로 된 통로입니다. VLAN 이 N개 일때 스위치간 링크는 N개여야합니다. Trunking 은 VLAN 이 N개 여도 하나의 통합 링크를 통해서 패킷을 보내는 방식입니다. 하나의 통합 링크를 통해 보내므로 각 패킷이 포함된 VLAN 정보를 알수 없습니다. 그래서 패킷에 자신의 VLAN 정보를 넣어 보내는 방식을 사용합니다. 이렇게 자신의 VLAN 정보를 붙여 보내는것을 Tagging 이라고 합니다.



여러 대의 스위치가 서로 연결되어 있는 네트워크에서 VLAN을 사용할 때 스위치는 서로 VLAN 정보를 주고 받아야합니다. 스위치간 연결된 포트간 세크먼트를 VLAN Trunking을 사용하여 해결합니다. VLAN Trunking이 만들어지면 스위치는 VLAN Tagging 이라는 과정을 이용하여 VLAN 정보를 교환합니다.

즉, 다시말해 스위치하나가 아닌 서로 다른 스위치의 장비를 같은 VLAN 으로 묶고 싶을 때 VLAN 정보를 스위치간에 공유해야되는데 이 VLAN 정보를 공유하는 것이 Trunking 이고 Tagging 이란 과정을 이용해 교환하는 규칙이 VTP(VLAN Trunking Protocol) 입니다.

만일 Trunking 이 지원되지 않는다면 스위치에 만들어진 VLAN의 개수만큼 스위치끼리 링크가 필요할 것입니다.




Tagging

VLAN ID는 트렁킹 프로토콜에서 VLAN을 구분하는 용도로 사용합니다. 이렇게 VLAN에 ID를 붙여서 구분하는 방법을 프레임 태깅(Frame Tagging) 이라고 합니다. ethernet 에서 사용하는 가장 대표적인 프레임 태깅 기술은 다음과 같습니다.

즉, 패킷에 VLAN ID를 붙여 이 패킷이 어떤 VLAN 에 포함되있는지 구분하여 받을지 말지를 결정하는 역활을 합니다.
예를들어 2개의 스위치가 있고 1번 스위치의 VLAN 10번의 장비가 2번 스위치의 VLAN 10번에게 패킷을 보낼때 1번 스위치의 장비는 패킷에 VLAN 10 번 이라는 Tag를 달아 2번 스위치에게 보내주게 됩니다. 2번 스위치는 이를 받아 풀어보면(untag) 이 패킷이 VLAN 10번에서온 패킷이라는 것을 알고 자신의 VLAN 10에 포함된 장비에게 이 패킷을 보냅니다. 다른 VLAN 에게는 이 패킷을 보내지 않습니다. Tag는 패킷이 어떤 VLAN에서 보내졌는지 알기위한 수단으로 사용합니다.

 - ISL(Inter-switch link) : 시스코 전용 트렁킹 프로토콜. 카탈리스트 1700과 같이 예전 스위치 모델에서만 지원합니다.

 - IEEE 802.1Q : IEEE 표준 트렁킹 프로토콜. 대부분의 카탈리스트 스위치 모델에서 지원합니다.





Trunk 의 필요성

Ethernet Switched Network 의 확장성과 비용 절감을 위해 필요합니다.

VLAN ID 는 같으나 물리적인 위치가 다른 VLAN 사이에 통신이 필요한 경우 필요합니다.

Switch 사이에서 하나 이상의 VLAN Traffic들을 효과적으로 전송해야 할 경우 필요합니다.

여러 VLAN 이 하나의 라우터와 하나의 물리 인터페이스를 공유해야 할 경우 필요합니다.

여러 VLAN이 하나의 Server를 공유해야 하는 경우 필요합니다.




VTP Mode(모드)



1. Server (서버)

 - VLAN 생성, 삭제, 수정 가능, 자신의 VLAN 설정 정보를 다른 스위치에 전송

 - 다른 스위치에게서 받은 정보와 자신의 정보를 일치 시키며(VLAN database 동기화), 이를 다른 스위치에게 중계

 - 매주기로 VLAN 정보를 전달한다. (다른 스위치가 연결되었을때 이 스위치가 VLAN 정보를 먼저 요청하지 않는다.)

 - Default 설정시 server 로 동작


2. Client (클라이언트)

 - VLAN 생성/삭제 불가능, Server 에게 받은 VLAN 정보가 자신의 VLAN 정보보다 낮은 Revision Number 일 경우 자신의 VLAN 정보를 보내줌

 - 다른 스위치의 정보를 자신의 VLAN 정보와 일치시키고 다른 스위치에게 중계

 - Client 가 접속시 먼저 VLAN 정보를 요청하지 않는다.


3. Transparent (트랜스패런트)

 - 자신의 VTP정보를 다른 스위치에게 전송하지 않으며 다른 스위치에게서 받은 정보와 일치 시키지 않음

 - 다른 스위치에게서 받은 정보를 중계하며, 자신만 사용할 (LOCAL) VLAN을 생성/삭제 가능



switchport mode 명령어의 Trunk 설정

 인터페이스 하위 명령어 옵션

명령 설명 

 access

 트렁킹 사용을 막고, 포트가 항상 access 포트로 사용되게 한다.

 한 포트가 한개의 VLAN 정보를 이동시킬 수 있다.

 trunk

 항상 트렁킹 포트로 사용한다. 

 한 포트가 여러개의 VLAN 정보를 이동시킬 수 있다.

 dynamic desirable

 협상 메시지를 보내고 응답하며, 협상을 통해 트렁킹 사용 여부를 동적으로 선택한다. 만약 이웃 포트가 trunk, desirable, auto 모드로 설정되어 있다면 트렁크 포트로 설정된다. access 일 경우 액세스 포트로 설정된다.

 dynamic auto

 트렁크 협상 메시지를 수신하며, 협상 메시지에 반응해 트렁킹의 사용 여부를 선택한다. 만약 이웃 포트가 trunk, desirable 모드라면 트렁크 포트로 설정된다. 패스트와 기가비트 이더넷 포트에서 디폴트 모드이다. access 나 auto일 경우 액세스 포트로 설정된다.




VTP Prunig (프루닝)



필요 없는 브로드캐스트 트래픽이 트렁크 포트를 통하여 전송되는 것을 차단하는 것을 말한다.




Trunk 구성 방법


VLAN 설정

Switch Vlan Access Mode 설정

 Switch (CISCO SF 200E-24 24-Port 10/100 Smart Switch)
 Cisco eCos-2.0
 직접해봄

 VLAN 생성

(switch)$ vlan database
(switch) (Vlan)$ vlan {vlan-number}
(switch) (Vlan)$ vlan {vlan-number} name {vlan-name}

예)
(switch)$ vlan database
(switch) (Vlan)$ vlan 10
(switch) (Vlan)$ vlan 10 name ADMIN
(switch) (Vlan)$ vlan 20
(switch) (Vlan)$ vlan 20 name IPMI
(switch) (Vlan)$ vlan 30
(switch) (Vlan)$ vlan 30 name REPLICATION

 VLAN 생성 확인

(switch)$ show vlan
VLAN ID VLAN Name                        VLAN Type
------- -------------------------------- ---------
1       Default                           Default
10      ADMIN                             Static
20      IPMI                              Static
30      REPLICATION                       Static

 포트에 VLAN 배정

(switch)$ configure
(switch) (config)$ interface {port}
(switch) (interface e1)$ switchport mode access
(switch) (interface e1)$ switchport access vlan {vlan-number}

예)
(switch)$ configure
(switch) (config)$ interface e4
(switch) (interface e1)$ switchport mode access
(switch) (interface e1)$ switchport access vlan 20

 포트 배정 확인

(switch)$ show interfaces switchport <port-type><port>


예)
switch)$ show interfaces switchport e1

Port: e1
VLAN Membership mode:Access Mode

Operating parameters:
PVID: 10
Ingress Filtering: Enabled
Acceptable Frame Type: Untagged
Default Priority: 0

Port e1 is member in:

VLAN    Name                              Egress rule   Type
----    --------------------------------- -----------   --------
10      ADMIN                             Untagged      Static

Static configuration:
PVID: 10
Ingress Filtering: Enabled
Acceptable Frame Type: Untagged

Port e1 is statically configured to:

VLAN    Name                              Egress rule
----    --------------------------------- -----------
10      ADMIN                             Untagged



Switch Vlan Trunk Mode 설정


 Switch (CISCO SF 200E-24 24-Port 10/100 Smart Switch)

 Cisco eCos-2.0
 직접해봄

 VLAN 생성

(switch)$ vlan database
(switch) (Vlan)$ vlan {vlan-number}
(switch) (Vlan)$ vlan {vlan-number} name {vlan-name}

예)
(switch)$ vlan database
(switch) (Vlan)$ vlan 10
(switch) (Vlan)$ vlan 10 name ADMIN
(switch) (Vlan)$ vlan 20
(switch) (Vlan)$ vlan 20 name IPMI
(switch) (Vlan)$ vlan 30
(switch) (Vlan)$ vlan 30 name REPLICATION

 VLAN 생성 확인

(switch)$ show vlan
VLAN ID VLAN Name                        VLAN Type
------- -------------------------------- ---------
1       Default                           Default
10      ADMIN                             Static
20      IPMI                              Static
30      REPLICATION                       Static

 포트에 VLAN 배정

(switch)$ configure
(switch) (config)$ interface {port}
(switch) (interface e1)$ switchport mode trunk
(switch) (interface e1)$ switchport trunk allowed vlan add {vlan-number}

예)
switch)$ configure
(switch) (config)$ interface e20
(switch) (interface e1)$ switchport mode trunk
(switch) (interface e1)$ switchport trunk vlan add 10
(switch) (interface e1)$ switchport trunk vlan add 20
(switch) (interface e1)$ switchport trunk vlan add 30

 포트 배정 확인

(switch)$ show interfaces switchport <port-type><port>

예)

switch)$ show interfaces switchport e20

Port: e20
VLAN Membership mode:Trunk Mode

Operating parameters:
PVID: 1
Ingress Filtering: Enabled
Acceptable Frame Type: Admit All
Default Priority: 0

Port e20 is member in:

VLAN    Name                              Egress rule   Type
----    --------------------------------- -----------   --------
1       Default                           Untagged      Default
10      ADMIN                             Tagged        Static
20      IPMI                              Tagged        Static

Static configuration:
PVID: 1
Ingress Filtering: Enabled
Acceptable Frame Type: Admit All

Port e20 is statically configured to:

VLAN    Name                              Egress rule
----    --------------------------------- -----------
1       Default                           Untagged
10      ADMIN                             Tagged
20      IPMI                              Tagged








linux VLAN 설정 (switch port를 trunk mode로 하였을시)

만약 스위치의 포트 설정이 trunk 로 되어 있다면 NIC 네트워크 인터페이스를 통해 나가는 패킷에 vlan id를 붙여야(tagging) VLAN 을 설정한 스위치가 해당 패킷의 VLAN 을 알수 있다. 만약 네트워크 인터페이스에 vlan id 태깅 작업을 하지 않는다면 해당 인터페이스에서 나가는 패킷이 포함된 VLAN 정보를 알수 없어 목적지에 도달하지 못한다.(vlan id 태깅을 하지 않으면 스위치 기본 vlan 인 vlan id 1 로 판단한다.)

따라서 운영체제의 네트워크 인터페이스에 vlan 설정을 해주어야 한다.

이해를 돕기 위해 예를 들어보자. 스위치의 1번,2번 포트를 vlan 10으로 묶었다. 1번과 2번에는 리눅스 장비가 연결되어 있다. 만약 1번 장비의 운영체제에 vlan 설정을 하지 않고 2번 장비를 목적지로 패킷을 보낸다면 vlan id 가 tagging 되지 않고 스위치에 도달할 것이다. 스위치는 받은 패킷의 vlan 정보를 확인했는데 vlan 정보가 없다. 스위치는 이것을 default vlan id 1 로 판단하고 목적지를 찾을 것이다. 스위치 2번 포트는 vlan id 10으로 설정되어있기 때문에 목적지에서 제외한다. 결국 이 패킷을 갈곳을 잃는다.

 Ubuntu (직접해봄)

 switch port 설정을 trunk 모드로 하였을시

설치

sudo apt-get install vlan


설정

8021q 커널 모듈 로드

sudo modprobe 8021q


8021q 커널 모듈 로드 확인

sudo lsmod | grep 8021q

 

vlan id 10을 위한 새로운 인터페이스 생성

sudo vconfig add eth1 10


새로운 인터페이스에 주소 할당

sudo ip addr add 10.0.0.1/24 dev eth1.10


재부팅시에도 vlan 설정을 유지하기 위한 설정

부팅시 커널에 모듈 추가

sudo su -c 'echo 8021q >> /etc/modules'


새로운 인터페이스에 대한 설정값을 /etc/network/interfaces에 설정

auto eth1.10
iface eth1.10 inet static
address 10.0.0.1
netmask 255.255.255.0
vlan-raw-device eth1 


network 서비스 재실행

service networking restart



Fedora/Centos/RHEL (직접해보지는 않음)

 8021q 커널 모듈 확인

lsmod | grep 8021q


 8021q 커널 모듈 로드

modprobe 8021q



/etc/sysconfig/network-scripts/ifcfg-ethN (N은 숫자임) 에 물리 네트워크 인터페이스 설정은 아래와 같다.
물리 네트워크를 통해 vlan tagging 패킷을 보낼것이다. 이때 물리 네트워크 인터페이스에 대한 가상 인터페이스를 하나 만들어야한다.

DEVICE=ethN #예를 들어 eth0
TYPE=Ethernet
BOOTPROTO=none
ONBOOT=YES


ethN 에 대한 가상 인터페이스 설정, 만약 vlan id 가 192 라면 아래와 같이 설정한다.

DEVICE=ethN.192   #예를 들어 eth0.192
BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.1.1
NETMASK=255.255.255.0
USERCTL=no
NETWORK=192.168.1.0
VLAN=yes


네트워크 서비스 재실행

systemctl restart network.service



다른 방법으로는 vlanX 인터페이스를 사용하는 방법이다.

VLAN=yes
VLAN_NAME_TYPE=VLAN_PLUS_VID_NO_PAD
DEVICE=vlan5
PHYSDEV=eth0
BOOTPROTO=static
ONBOOT=yes
TYPE=Ethernet
IPADDR=10.254.5.254
NETMASK=255.255.255.0




VLAN 활용

 테스트를 위해 여러 서버를 구축했고 스위치가 하나일때 이미 이 스위치는 서비스를 위해 활용되고 있을때 vlan 을 사용한다. 테스트를 위한 서버들을 vlan 으로 묶으면 서비스하는 서버들에게 영향을 주지 않기 때문이다. 혹시라도 테스트 서버에서 트래픽을 유발하거나 임의의 접근루틴등이 있어도 테스트 서버들은 vlan 으로 묶여있기 때문에 문제가 발생되지 않는다.

외부와 통신하는 서버 즉 실제 사용자와 인터넷을 이용하여 통신하는 서버가 몇개 있고 백엔드에서 작업을 처리해 주는 서버가 있다고 하자. 백엔드에서 작업을 처리해 주는 서버는 인터넷 망에 연결될 필요가 없고 인터넷 망에 연결되어있다면 보안적으로 문제가 발생할 수 있다. 이런 백엔드에서 작업을 처리해주는 서버들을 vlan으로 묶는 것이다.



VALN 간 통신

이것은 링크로 대신하겠다. (만질수 있는 라우터가 없기에.. 테스트를 못하겠다.)

 http://blog.naver.com/PostView.nhn?blogId=gudwlsl1213&logNo=20129196992&redirect=Dlog&widgetTypeCall=true

 http://iorora.web-bi.net/tech/study/inter-vlan.pdf




보통의 클라이언트, 서버 모델은 클라이언트의 연결 요청이 이루어 지고 클라이언트의 작업요청에 대해 서버가 응답해주는 형태이다. 즉 서버는 클라이언트가 자신에게 요청을 하지 않는다면 응답해주지 않는다.

예를 들어 하나의 웹페이지가 있다. 이 웹페이지는 공지사항을 띄워주는 웹페이지다. 기본적인 웹페이지는 새로운 공지사항이 생겼는지 확인하기 위해 유저가 새로고침을 누를 것이다. 혹은 새로고침을 주기적으로 반복하는 스크립트를 넣을 것이다. 공지사항이 새로 올라오지도 않았는데 새로고침을 통해 서버에게 요청을 보내야 하고 이는 서버에 많은 부담을 준다.

push  server는 클라이언트의 요청이 오면 응답해주는 방식이 아닌 서버가 클라이언트에게 공지사항과 같은 무엇인가 통지해주기 위한 방법이다. 다시 말해 클라이언트의 요청이 없이도 서버는 클라이언트에게 응답하는 방식이다.

일반적인 소켓 프로그래밍에서는 클라이언트와 서버가 연결이 되면 그 연결이 유지되므로 push server는 일반적인 서버모델과 다를 바가 없다. 하지만 HTTP 프로토콜을 사용하는 웹프로그램/웹페이지 같은 경우는 HTTP 프로토콜의 특성상 연결이 유지되지 않는다. 서버가 클라이언트에게 통지해줄수 있는 방법이 없다는 것이다. HTTP 서버는 매우 수동적인 존재이다.

이런 HTTP 프로토콜의 특성때문에 실제 push server 구현은 되지 않는다. 하지만 push server 의 효과와 비슷한 모델들이 등장한다. 
웹서버의 push server 모델은 POLLING, LONG POLLING, STREAM 이 존재한다.
POLLING 은 클라이언트가 끊임없이 웹서버에게 새로운 내용이 있는지 물어보는 방식이다.
LONG POLLING 은 클라이언트가 웹서버에게 새로운 내용이 있는지 물어보았을 때 웹서버에 새로운 내용이 없다면 대답해주지 않다가 새로운 내용이 생기면 이때 대답해주는 방식이다.
STREAM은 웹서버의 응답 헤더의  content-length 를 이용한 방법이다. 응답헤더의 content-length 가 존재하지 않는다면 클라이언트는 연결이 끊길때 까지 응답을 받아들인다. 즉 한번의 요청에 대해 끊임없는 응답을 보내는 방식이다. 이 방식은 한번의 요청만 있으면 되므로 효율은 좋지만 error 처리에 매우 취약하다.

요즘은 대체로 LONG POLLING 방식을 추구하는 편이다.

나도 구현해 본 적이 없고 글로만 이해했기 때문에 아주간단하게(나만 볼수있게) 정리해 봤다. 아래의 참고사이트를 통해 이해하라. 실습을 통해 구현하게 되면 블로그에 추가내용을 올리겠다. (자바나 서블릿등 웹프로그래밍의 기본지식은 필요하다.)

http://mygony.com/archives/2535

http://ajaxpatterns.org/HTTP_Streaming

http://www.ape-project.org/ - ajax push engine

http://www.caucho.com/resin-3.1/doc/resin-comet.xtp - Resin: Application Server | Java EE Web Profile Certified Cloud Optimized | DevOps Friendly

http://en.wikipedia.org/wiki/Push_technology

http://en.wikipedia.org/wiki/Comet_(programming)

http://stackoverflow.com/questions/333664/simple-long-polling-example-code - long polling simple example

http://ko.wikipedia.org/wiki/XAMPP - simple web server

COMET 구현 기법 
http://www.uengine.org:8088/wiki/index.php/Comet_%EA%B5%AC%ED%98%84_%EA%B8%B0%EB%B2%95

Push technology
http://en.wikipedia.org/wiki/Push_technology 

COMET 소개/ 이벤트 중심 웹개발
http://www.ibm.com/developerworks/kr/library/wa-reverseajax1/index.html
http://www.ibm.com/developerworks/kr/library/wa-reverseajax2/index.html
http://www.ibm.com/developerworks/kr/library/wa-reverseajax3/index.html
http://www.ibm.com/developerworks/kr/library/wa-reverseajax4/index.html
http://www.ibm.com/developerworks/kr/library/wa-reverseajax5/index.html

commet 제품 비교 사이트
http://cometdaily.com/maturity.html



 예제 참고

http://www.uengine.org:8088/wiki/index.php/Comet_%EA%B5%AC%ED%98%84_%EA%B8%B0%EB%B2%95#XHR_long_polling
http://www.zeitoun.net/articles/comet_and_php/start
http://blog.naver.com/PostView.nhn?blogId=playse444&logNo=150128458138
http://helloworld.naver.com/helloworld/1052 - 강추
http://breathair.tistory.com/113 - XMLHttpRequest 설명
http://streamhub.blogspot.kr/2009/07/tutorial-building-comet-chat.html
http://www.ibm.com/developerworks/kr/library/wa-reverseajax1/ - XHR, multipart
http://tkjeon.tistory.com/6
http://stackoverflow.com/questions/5894019/accurate-long-polling-example
http://stackoverflow.com/questions/333664/simple-long-polling-example-code
http://www.ibm.com/developerworks/kr/library/wa-reverseajax2/index.html - WebSocket
http://jjeong.tistory.com/m/post/view/id/526 - multipart/x-mixed-replace 와 xhr 사용

http://dsheiko.com/weblog/html5-and-server-sent-events - html5 server sent event

















정리

1. comet 정의

 comet 은 브라우저에서 명시적인 요청없이 웹서버에서 브라우저로 데이터를 push 하는 웹 어플리케이션 모델이다. 

comet은 이 상호작용을 위한 다양한 기술을 포함하는 용어이다.

2. 방식

 Polling
 

 

 기본적인 http 프로토콜이다. 특정 요청에 대해 이벤트가 있던 없던 응답을 보낸다.

요청에 대한 서버 부담이 크지 않거나 실시간 메시지 전달이 크게 중요하지 않은 서비스에 적합하다.
요청주기는 조절 할 수 있지만 요청주기가 너무 길다면 서버 상태 변화를 빠르게 인지하지 못하고 요청주기가 너무 짧다면 서버 상태 변화는 빠르게 인지하지만 서버에 많은 부하를 줄 수 있다. 전체적으로 서버부하와 네트워크 트래픽을 많이 발생시킨다.

네트워크 프로그래밍의 non-block 방식이랄까?


Long Polling 

 


 Polling 모델과 차이점은 클라이언트의 요청에 대해 서버에서 이벤트 발생시에만 응답을 준다는 것이다. Polling 과 Long Polling 의 차이점은 즉 클라이언트 요청에 대한 서버의 대처방식이다.
Long Polling 은 실시간 메시지 전달이 중요하고 서버의 상태 변경이 빈번히 발생하지 않는 서비스에 적합하다. Polling 방식에 비해 요청/응답 트랜잭션이 덜 하다. 따라서 서버와 네트워크에 부하가 적다.

네트워크 프로그래밍에서 block 방식이랄까? 조금 다른 점은 네트워크 block 방식에서는 응답을 받았다고 연결을 끊지 않지만 Long Polling 에서는 응답을 받으면 연결을 끊고 다시 연결을 한다는 점이다.




Streaming 

 

 Streaming 은 요청후에 연결을 지속하여 계속적인 응답을 받는 것이다. Polling, Long Polling 과 다르게 응답을 받은후 연결을 종료하지 않는다. 서버는 상태변화(이벤트)시 이 스트림을 통해 응답을 지속적으로 보낸다. 이 모델의 경우 요청이 단 한번만 이루어지기 때문에 서버와 네트워크 트래픽에 부하를 덜 준다. 하지만 응답데이터의 시작과 끝을 알수 없기 때문에(스트림이니까) 데이터의 유효성을 체크하는 루틴이 필요하다.

네트워크 프로그래밍의 단방향 모델이랄까? 

3. 세부방식

 모델 구현법 

 Polling & Long Polling

 url call

 XMLHttpRequest 
 Streaming 

 XMLHttpRequest

 IFrame 

 Htmlfile 

 Multipart 

 Server Sent Event 
 Web Socket

Polling 과 Long Polling 은 클라이언트에서 구현이 동일하다. Long Polling 은 서버쪽의 특별한 기능이 필요하다.(응답을 이벤트 발생시에만 보내는 것)


 구현법설명 

 url call (callback polling)

 가장 기본적인 브라우저 구현방식이다. 
 대체로 서버에서 <script> 코드와 데이터를 클라이언트로 보내어 클라이언트(브라우저)에서 해석 실행하게 하는 원리이다. 이를 callback polling 이라 한다.

 XMLHttpRequest 

 XHR은 callback polling 과 다르게 script 코드 없이 필요 데이터만을 보낼수 있다. 대체로 xml, json 등으로 응답 데이터를 전달한다.
 XHR은 보안상 제약이 있다. 하지만 에러와 상태를 더 정교하게 다룰 수 있다는 장점이 있다. Long Polling 과 Streaming 모델에서 사용된다.

 IFrame

 inner frame 은 부모 frame 내에 자식으로 frame 을 넣고 요청과 응답을 자식 frame에서 처리하도록 하는 것이다.
 iframe 을 지원하는 모든 브라우저에서 작동한다.
 http/1.1 의 chunked 인코딩 방식을 기반으로 서버와 연결을 맺어둔다.
 모든 연결과 데이터가 html 태그를 통해 브라우저에 의해 처리되므로 안정적인 오류 처리를 구현하거나 연결 상태를 추적할 방법이 없다. 그래서 어느 쪽에서든 연결이 끊어지는 때를 알 수 없다.
 iframe 을 이용하므로 웹 부라우저의 상태 표시줄에 연결 중임을 계속 표시하고 서버 응답시 클릭하는 소리가 난다.

 htmlfile

 htmlfile 이라는 ActiveX Object를 이용하는 방식으로 IFrame의 단점(클릭소리, 연결중 표시)을 해결 할 수 있다.
 ActiveX 는 마이크로소프트의 인터넷 익스플로러에서만 지원하므로 익스플로러에서만 동작한다. 

 Multipart

 서버에서 응답을 content-type 을 multipart/x-mixed-replace 로 보내는 방식으로 multipart/x-mixed-replace 은 서버 푸시와 http 에서 스트림 구현을 위해 개발되었다.
클라이언트(브라우저)는 XHR(XMLHttpRequest) multipart 를 이용하여 구현할 수 있다.
현시 xhr.multipart=true
; 한다.

 Server Sent Event 
(event-source object)

 HTML5 에 정의된 서버 푸시 기술의 표준이다.
 서버는  content-type 이 application/x-dom-event-stream 인 데이터를 보내면 클라이언트(브라우저)는 이벤트와 매칭되는 callback 함수를 호출한다.

 Web Socket

 HTML5에 처음 도입된 WebSocket은 comet 보다 훨씬 더 최신의 기술이다. WebSocket은 양방향 전이중 통신 채널을 사용하며 많은 브라우저에서 지원하고 있다. WebSocket은 몇가지 특수 헤더와 함께 WebSocket 핸드쉐이크라고 하는 HTTP요청을 통해 연결이 열린다. 이 연결은 계속 유지되며 원시 TCP 소켓을 사용하는 것처럼 javascript에서 데이터를 쓰고 수신할 수 있다.
 기본 comet 에 비해 WebSocket을 지원하는 클라이언트는 속도가 더 빠르고 요청을 덜 생성하며 대역폭을 덜 소비한다.
 WebSocket 을 지원하지 않는 브라우저의 경우 일부 라이브러리는 FlashSocket(Flash를 통한 소켓)으로 변경할 수 있다. 이런 라이브러리는 보통 공식 WebSocket API를 제공하며 웹사이트에 포함된 숨겨진 Flash 컴포넌트에 호출을 위임하여 이를 구현한다. 


 FlashSocket (참고)

 장점 : FlashSocket은 HTML5 WebSocket을 지원하지 않는 브라우저에서도 WebSocket 기능을 투명하게 제공한다.

 단점 : flash 플러그인을 설치해야한다.(보통 모든 브라우저에는 이미 설치 되어 있음)
          flash 컴포넌트가 도메인 권한을 포함한 정책 파일을 검색하는 HTTP 요청을 할 수 있 도록 방화벽의 포트 843이 열려 있어야한다. 포트 843에 연결할 수 없는 경우 라이브러리를 변경하거나 오류가 발생해야한다. 이런 처리를 모두 수행하는 데는 라이브러리에 따라 최대 3초가 걸리므로 웹 사이트의 속도를 떨어뜨리게된다.
         클라이언트가 프록시 서버 뒤에 있는 경우 포트 843에 대한 연결이 거부될 수 있다.

 WebSocketJS 프로젝트는 브릿지를 제공한다. 이 프로젝트에는 최소한 Flash 10이 필요하고 Firefox 3, internet Explorer 8, 9 까지 Websocket 지원을 제공한다.


클라이언트(브라우저)는 XMLHttpRequest 를 Polling, Long Polling, Streaming 구현시 다 사용할 수 있다. 좋다.
WebSocket 등 더 좋은 기술을 사용할 수 있지만 지원하지 않는 브라우저를 위해 차선택인 comet 모델을 지원할 수 있도록 하는 것이 좋다.


4. 예제


Long Polling - url 호출 (JSONP)

클라이언트 요청
 var script = document.createElement('script'); script.src = 'http://domain.com/path/to/server.cgi?callback=callback_function_name' ;

서버 응답
callback_function_name( {some:data, in_any:format, you:want} );

 클라이언트는 script 를 요청하고 url 에 callback 파라미터를 통해 처리함수를 넘겨준다.

서버는 callback 파라미터로 넘어온 처리함수를 호출할수 있도록 하고 내부 인자로 JSON 혹은 XML , 일반 데이터를 얻어 보내준다. 위와 같이 JSON 데이터를 콜백함수로 감싸는 것을 JSONP 라고 한다.

클라이언트는 script 코드를 실행하므로써 자신이 원하는 처리를 할 수 있다.

구현은 간단하나 서버의 응답이 script 코드를 넘겨주므로 우리가 원하던 JSON, XML 데이터로만으로 받을 수는 없다.

<script> 태그의 특성상 src에 지정한 리소스를 모두 읽어온 후에 해석하고 실행하기 때문에 스트리밍 방식을 사용할 수 없다. 하지만 <iframe> 태그는 src에 지정한 리소스를 모두 읽지 않아도 받은 데이터를 즉각즉각 해석하기 때문에 스트리밍이 가능하다.

 장점 : 여러 브라우저에서 사용 가능하며 포트, 도메인등의 제약을 받지 않는다.

 단점 : 에러나 http의 상태를 다룰 수 없고 스트리밍 방식을 사용할 수 없다.


Long Polling - XMLHttpRequest (XHR) 

클라이언트 요청
 function longPoll(url, callback) {
   var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function() {
   if(xhr.readyState == 4) {

// 응답이 완료되면 서버로
//재연결 요청보내기
   callback(xhr.responseText);
   xhr.open('GET', url, true);
   xhr.send(null);
  }
}

// 서버로 요청 연결하기

xhr.open('POST', url, true);

xhr.send(null);

}

서버 응답 - XML , JSON , 일반 데이터
{some:data, in_any:format, you:want} 


 url 호출을 이용한 script 방식과 비슷하나 서버 응답 데이터가 script 로 감싸여 있지 않는다. 즉 데이터 그 자체로만으로 반환된다.

 XMLHttpRequest 라는 객체를 사용한다.

 장점 : 에러와 http 상태를 더 정교하게 다룰 수 있다.

 단점 : 보안 제약이 있다.(몇몇 브라우저에서 XHR을 통한 스트리밍이 되지 않는다. )


 Streaming - XHR streaming

 클라이언트 요청

function xhrStreaming(url, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  var lastSize;

// 최신 텍스트를 가져오기 위한 위치
  xhr.onreadystatechange = function() {
  var newTextReceived;

if(xhr.readyState > 2) {
  // 최신 텍스트 가져오기
  newTextReceived =  xhr.responseText.substring(lastSize);

  lastSize = xhr.responseText.length;
  callback(newTextReceived);

   }

if(xhr.readyState == 4) {

// 응답이 완료되면
//새로운 요청을 다시 만든다

  xhrStreaming(url, callback);

  }

}

// 서버로 요청 연결하기
  xhr.send(null);


서버 응답 - XML , JSON , 일반 데이터 (Content-Length = 0)
{some:data, in_any:format, you:want} 

 XHR Long Polling 과 비슷하나 readyState 상태가 완료 상태가 아닌 데이터의 일부를 받았을때 처리를 한다는 것에서 다르다. 또한 서버는 연결을 끊지 않고 계속적으로 데이터를 보낸다.

 데이터의 앞과 뒤가 없어 데이터의 유효성을 체크하는 기능이 필요하고 응답 데이터는 계속적으로 쌓이므로 주기적으로 데이터를 없애거나 주기적으로 연결 해제후 다시 연결하는 방식을 사용해야한다.

원리 :
1. onreadystatechange 이벤트가 발생하면 readyState 변수를 확인한다.
2. readyState 변수가 3(혹은 4) 일때, 요청 연결 후부터 현재까지 서버로부터 받은 결과에 접근한다.
3. 결과 중 마지막 이벤트에 해당하는 텍스트를 가지고 온다.


  장점 : 응답 결과의 텍스트 뿐 아니라 헤더에도 직접 접근할 수 있고 장애도 비교적 쉽게 처리할 수 있다.
  단점 : 몇몇 브라우저에서 readState 가 3일때 보안상의 이유로 응답결과에 접근하지 못한다.


 Streaming - IFrame Streaming

 클라이언트 요청

예1)

function foreverFrame(url, callback) {
  var iframe = body.
  appendChild(
  document.createElement("iframe"));
  iframe.style.display = "none";
  iframe.src = url + "?callback=parent.foreverFrame.callback";
  this.callback = callback;
}

예2)

var iframe = document.getElementById(iframe_id);
if (!iframe) {
    iframe = document.creatElement('iframe');
    iframe.id = iframe_id;
    document.body.appendChild(iframe);
}

iframe.style.position = 'absolute';
iframe.style.top = '-100px';
iframe.style.left = '-100px';
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.src = 'http://server/push.cgi';


서버 응답

<script>parent.foreverFrame.callback("the first message");</script>
<script>parent.foreverFrame.callback("the second message");</script>

 내부 프레임이라는 html 객체를 이용한다. iframe 은 frame 내부에서 보이지 않게 요청을 보내고 응답결과를 실행한다.
 브라우저 특성에 따라 <iframe>의 display 속성이 none 일 경우 iframe을 폼 전송의 target으로 설정할 수 없으므로 크기를 매우 작게 만들어 안보이는 영역에 두는 방식을 사용하는 것이 좋다.

서버 응답의 경우 주기적으로 clean 코드를 두어 계속적으로 쌓이는 <script> 코드를 제거해주어야한다.

 장점 : 폭넓은 호환성이 있다. 왠만한 오래된 브라우저도 <iframe>태그를 지원한다.

 단점 : 에러를 제대로 다룰 수 없다. http 상태를 다룰 수 없다 . 모바일 브라우저에서 사용할 수 없다. 웹 브라우저의 기본 보안 정책인 동일 출처 정책(same-Origin Policy)의 영향을 받으므로 도메인이나 스키마, 포트가 다른 문서간에는 사용할 수 없다.
클릭소리와 상태 표시줄에 연결중임을 계속 표시한다.


 Streaming - htmlfile streaming

 클라이언트 요청

예1)

// we were served from child.example.com but
// have already set document.domain to example.com
var currentDomain = "http://exmaple.com/";
var dataStreamUrl = currentDomain+"path/to/server.cgi";
var transferDoc = new ActiveXObject("htmlfile"); // !?!

// make sure it’s really scriptable
transferDoc.open();
transferDoc.write("<html>");
transferDoc.write("<script>document.domain='"+currentDomain+"';</script>");
transferDoc.write("</html>");
transferDoc.close();

 // set the iframe up to call the server for data
var ifrDiv = transferDoc.createElement("div");
transferDoc.appendChild(ifrDiv);

// start communicating
ifrDiv.innerHTML = '<iframe src="'+dataStreamUrl+'"></iframe>';


예2)

function foreverFrameByHtmlfile(url, callback) {
// 명시적인 가비지 콜렉터 호출을 위해
//'var transferDoc...'라고 하지 않음 

transferDoc = new ActiveXObject("htmlfile");
transferDoc.open();
transferDoc.write("<script>" + "document.domain='" + document.domain + "';" +"</script>");
transferDoc.close();
var ifrDiv = transferDoc.createElement("div");
transferDoc.body.appendChild(ifrDiv);
ifrDiv.innerHTML ="<iframe src="+ url +"></iframe>";
transferDoc.callback = callback;

}

function foreverFrameClose() { 
// transferDoc 참조를 없애고,
// 명시적으로 가비지 콜렉터를 호출함
transferDoc = null;
CollectGarbage();

}

// 페이지가 이동되거나
// 창이 닫힐 때 호출함수 지정
window.onunload = foreverFrameClose; 


<iframe> 대신 ActiveX 콘트롤의 htmlfile 객체를 이용하는 방법이다. 따라서  인터넷 익스플로러의 경우만 사용할 수 있다.

기본적인 내용은 iframe 와 동일하다.


 Streaming - multipart streaming (XHR multipart streaming)

 클라이언트 요청

//we create a norma xhr object (this is the mootools way but can be anything)
this.tunnel = new Request({
url: this.options.urlServer,
method: 'get',
header: ''
});

//we active the multipart option of the xhr object
this.tunnel.xhr.multipart = true ;
//we change the onload function, and everytime we got a readyState == 4, it means that we get a new part of data !
this.tunnel.xhr.onload = function (event) {
if (event.target.readyState == 4 ) {
alert('data pushed :'+event.target.responseText) ;
this.fireEvent('dataPushed', [event.target.responseText, event.target.responseXML])
}
else {
this.fireEvent('error', "Error xhr : readyState: "+event.target.readyState)
}
}.bind(this.tunnel);



서버 응답
코드 예)
this.pw.print("Content-Type: multipart/x-mixed-replace;boundary=myboundary");
this.pw.print("Content-Type: text/json\r\n\r\n"+_the_data_to_push+"\r\n\r\n--myboundary\r\n"); 


응답 예)

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=--myboundary

--myboundary
Content-Type: text/json
Content-length: 12345

[json data]

--myboundary
Content-Type: text/json
Content-length: 45678

[json data]

...


 멀티파트는 데이터를 구분자(위의 예제에서는 --myboundary)로 나누므로써 데이터의 시작과 끝을 정확히 표현하고 스트리밍 방식으로 데이터를 계속 넘겨주기에 좋은 방법이다.

multipart 의 자세한 내용은 google 검색을 활용하라.

 장점 : 여러 브라우저에서 지원하고 ,  XHR 의 보안제약인 readyState 가 3 일때 응답 데이터에 접근하지 못하는 제약도 피한다.

 단점 : 


 streamint - Server Sent Events (the event-source object)

클라이언트 요청

예1)

document.getElementsByTagName("event-source")[0] .addEventListener(" a_specific_id ", eventHandler, false);
function eventHandler(event)
{
    // Alert time sent by the server
    alert(event.data); 
}

예2)

//create the event-source object in the dom
this.tunnel = document.createElement("event-source");
//we add a specific listenenr on the event-source.
this.tunnel.addEventListener("a_specific_id", this.decodeJSON.bindWithEvent(this), false);
//we set the src attribute = the server URL
this.tunnel.setAttribute("src", "your java comet server url");

//finaly the .decodeJSON function called every time a data will be pushed by the server
this.decodeJSON: function (_textResponse) {
_textResponse = _textResponse.data ;
}


예3)

var eventSource = new EventSource("server.asp");
eventSource.addEventListener("message", function(e){          
      alert(e.data);
    }
,false);

서버 응답

코드 예)
예1)
this.pw.print("Content-Type: application/x-dom-event-stream");
this.pw.print("Event: a_specific_id\ndata: "+_the_data_to_push+"\n\n");

예2)

<?php
header("Content-Type: application/x-dom-event-stream");
while(true) {
    echo "Event: a_specific_id\n";
    echo "data: data_to_push\n";
    echo "\n";
    flush();
}
?>


예3)

<?php
header("Content-Type: text/event-stream\n\n");
echo 'data: ' . time() . "\n";
?>


응답 데이터 예)

Content-Type: application/x-dom-event-stream

Event:a_specific_id\r\n
data:data_to_push\r\n\r\n


  html5 에 정의된 서버푸시 기술의 표준인 server sent events api를 사용한다. 
  application/x-dom-event-stream 의 경우 이벤트 명을 명시적으로 표현(Event:a_specific_id)하므로써 클라이언트에서 처리루틴 결정을 확실히 할 수 있다. (Content-Type: text/event-stream 은 이벤트 명이 포함되 있지 않다.)

 장점 : 심플한 자료구조와 인터페이스, 통신 매커니즘을 정의하고 있으며 일반적인 DOM 이벤트 형태로 수신 데이터를 처리 할 수 있는 등 서버푸시 구현이 매우 간단해진다.

 단점 : 


Streaming - WebSocket 

 클라이언트 요청

var ws = new WebSocket('ws://127.0.0.1:8080/async'); 
ws.onopen = function() { 
    // called when connection is opened 
}; 

ws.onerror = function(e) { 
    // called in case of error, when connection is broken in example 
}; 

ws.onclose = function() { 
    // called when connexion is closed 
}; 

ws.onmessage = function(msg) { 
    // called when the server sends a message to the client. 
    // msg.data contains the message. 
}; 

// Here is how to send some data to the server 
ws.send('some data'); 
// To close the socket:
ws.close();


서버 응답

jetty 컨테이너용 websocket 핸들러

public final class ReverseAjaxServlet extends WebSocketServlet { 
 @Override 
    protected WebSocket doWebSocketConnect(HttpServletRequest request,
                                           String protocol) { 
        return [...] 
    } 
}

WebSocket 구현
class Endpoint implements WebSocket { 

    Outbound outbound; 

    @Override 
    public void onConnect(Outbound outbound) { 
        this.outbound = outbound;    
    } 

    @Override 
    public void onMessage(byte opcode, String data) { 
        // called when a message is received 
        // you usually use this method 
    } 

    @Override 
    public void onFragment(boolean more, byte opcode, 
                           byte[] data, int offset, int length) { 
        // when a fragment is completed, onMessage is called. 
        // Usually leave this method empty. 
    } 

    @Override 
    public void onMessage(byte opcode, byte[] data, 
                          int offset, int length) { 
        onMessage(opcode, new String(data, offset, length)); 
    } 

    @Override 
    public void onDisconnect() { 
        outbound = null; 
    } 
}


Websocket 구현

class Endpoint implements WebSocket { 

    Outbound outbound; 

    @Override 
    public void onConnect(Outbound outbound) { 
        this.outbound = outbound;    
    } 

    @Override 
    public void onMessage(byte opcode, String data) { 
        // called when a message is received 
        // you usually use this method 
    } 

    @Override 
    public void onFragment(boolean more, byte opcode, 
                           byte[] data, int offset, int length) { 
        // when a fragment is completed, onMessage is called. 
        // Usually leave this method empty. 
    } 

    @Override 
    public void onMessage(byte opcode, byte[] data, 
                          int offset, int length) { 
        onMessage(opcode, new String(data, offset, length)); 
    } 

    @Override 
    public void onDisconnect() { 
        outbound = null; 
    } 
}

메시지 보낼때

if (outbound != null && outbound.isOpen()) {
    outbound.sendMessage('Hello World !');
}


 websocket 을 이용하여 데이터 스트림을 생성하고 마치 tcp/ip 통신을 하듯이 프로그래밍을 하면 된다.

 http://html5korea.co.kr/html5lec/900 여기를 참고하기 바란다.

 장점 : 

 단점 : 


참고 : http://breathair.tistory.com/113XMLHttpRequest(XHR) 설명 

 

 





  1. 준지 2013.09.15 01:17

    와......감탄하고 갑니다.....진심으로 감사합니다 이런 정리를 볼수있게 해주시다니....

  2. jagarian 2013.10.26 15:11

    웹소켓을 더 봐야 겠네요 좋은자료 감사합니다

  3. 추리가이 2016.05.11 10:48

    글 감사히 잘 보고 갑니다~^^

http://www.cs.cmu.edu/~srini/15-441/F01.full/www/assignments/P2/htmlsim_single/

http://www.cs.cmu.edu/~srini/15-441/F01.full/www/assignments/P2/htmlproj2_single/


네트워크 이론 간략히 설명한것

INADDR_ANY 찾다가 발견함.. 


Simulation Environment Overview 
15-441 Project 2 and 3, Fall 2001

1 Overview

In this document, we describe the simulation environment which you will be using in projects 2 and 3 for this class. The simulator implements the basic components of an operating system kernel, as well as the socket, transport, link and physical layers. You will be responsible for adding the network layer to the kernel. The details of your project assignments can be found in the project handouts.

Figure 1: Simulator Overview: The kernel of each node is implemented as a separate UNIX process. Each application running on a node is in a separate process.

\includegraphics[height=3.5in, keepaspectratio]{fig-simpict.eps}

Figure 1 shows a picture of a sample simulated network. The kernel of each node in the network is a separate UNIX process. An application running on top of a node is a UNIX process separate from the kernel process. The fact that each node is implemented as a separate process enables you to simulate communications between nodes even though all the nodes are actually running on the same machine. Applications are implemented as separate processes so that they can be started after the simulation is already running (i.e. the kernel on each node is running) and so that more than one application can be run on the same node.

Each node has its own operating system kernel which you implement. Some nodes utilize all the layers of the network stack implemented in your kernel, and there are applications running on top of them (Nodes 2 and 3 in the figure). These nodes represent end-systems or communication endpoints. Other nodes, e.g. Node 1, only use the physical, link and network layers of the network stack. These nodes are routers. They are responsible only for forwarding packets, and since forwarding is a function provided by the network layer, they do not need to use the layers above the network layer. Endpoints on the other hand, do need to have all layers of the network stack since packets that are sent and received by the application layer need to undergo processing by all layers below the application layer.

In this handout, we will use $PDIR to denote the project directory.

The project directory for project 2 will be: 
/afs/cs.cmu.edu/academic/class/15441-f01/projects/project2/

The project directory for project 3 will be: 
/afs/cs.cmu.edu/academic/class/15441-f01/projects/project3/

2 Building the kernel and running a network simulation

The support code for your projects provides an environment that emulates a simple machine with hardware-level network devices and a system call interface. It is provided to you in the form of a library: libkernel.a.

When building the kernel, all your files will be compiled and linked against the libraries we provide to form a single Solaris executable.

You will be using the simulator to simulate a network. Typically, a network consists of more than one node (otherwise it is not very interesting). A sample network configuration is shown in Figure 2. This configuration may be represented in the simulator as shown in Figure 1.

A script $PDIR/template/startkernel.pl is provided to help you bring your network up when you start the simulation. This script reads a network configuration file (see Section 2.1) that you specify and launches the appropriate number of kernels. Each kernel is started in its own xterm window. An optional second argument (debug) may be specified to startkernel.pl so that it runs each kernel within gdb. If you specify the debug option, you will have to start the kernel in gdb manually. You have to set the arguments for the kernel using ``set args ...'' (see the script for what arguments are needed). Then you can issue the command ``run''. If you don't specify the debug option, problems may be difficult to debug since when a kernel crashes, the xterm window corresponding to that kernel will close.

Figure 2: A sample network configuration.

\includegraphics[height=1.5in, keepaspectratio]{fig-config.eps}


2.1 Network configuration file

As mentioned above, you need to specify a network configuration file when you run a simulation. This configuration file specifies each node in the network along with all of its interfaces and their respective addresses, as well as all the links that exist between each node and other nodes in the network.

We use the network from Figure 2 to illustrate how network configuration files are built. Interface 1 on node R1 is connected to interface 1 on node R2, and interface 2 on node R1 is connected to interface 1 on node R3. The network configuration file for this network is the following:

# Configuration for Router 1
Router 1 {
     1  1.1.1.1  255.255.255.255
     2  1.1.2.1  255.255.255.255
     1:1 2:1
     1:2 3:1
}

# Configuration for Router 2
Router 2 {
     1  1.1.1.2  255.255.255.255
     2:1 1:1
}

# Configuration for Router 3
Router 3 {
     1  1.1.2.2  255.255.255.255
     3:1 1:2
}
As usual, lines that start with a ``#'' are comments and will be ignored by the simulator. Each interface on each router has its own IP address and netmask. The notation X:Y refers to interface Y on node X. Thus, the line ``1:1 2:1'' in the configuration entry for node R1, shown above, specifies that interface 1 on R1 should be connected to interface 1 on R2. Note that in this configuration, R2 and R3 are actually end points, not routers. However, the simulator requires the word ``Router'' for each node in the configuration file.

This sample configuration file is provided in $PDIR/template/network3.cfg. You can modify the sample or create your own configuration for testing purposes.

3 Building and running user programs

User programs can be run on each simulated node. Each user program is run as a separate user process as shown in Figure 1.

All user programs used with the simulator must be linked against the user libraries we provide (see the template Makefile for more details). There are two important issues regarding user programs:

  1. Use Main() instead of main(). The entry point of a user program is actually in the simulator, not the code you write. After the simulator is done with the necessary initializations, it invokes your Main() function. The Main() function is exactly the same as main(); that is, the usual argc and argv are still there.
  2. User programs must be run with ``-n i'' as the first argument. This argument is to specify that this user program should be run on node i. Note that the Main() function will not get this argument (i.e. the simulator will strip this argument before passing the arguments to Main()).
  3. You should close all open sockets before returning from Main(). If you forget to close an open socket, the simulator will not close it for you and future invocations of your program might fail.


4 The pbuf structure

A packet sent or received by an application is processed by several different layers in the network stack. In real BSD-style implementations, an mbufstructure is used for passing the packet between the different layers. In projects 2 and 3, you will be using a pbuf structure for building and passing packets between network stack layers. The pbuf structure is simplified version of the BSD mbuf.

The definition of the pbuf structure is the following:

    struct p_hdr {
            struct  pbuf *ph_next;    /* next buffer in chain */
            struct  pbuf *ph_nextpkt; /* next chain in queue/record */
            caddr_t ph_data;          /* location of data */
            int     ph_len;           /* amount of data in this mbuf */
            int     ph_type;          /* type of data in this mbuf */
            int     ph_flags;         /* flags; see below */
    };

    struct pbuf {
            struct p_hdr p_hdr;
            char         p_databuf[PHLEN];
    };
    #define p_next    p_hdr.ph_next
    #define p_nextpkt p_hdr.ph_nextpkt
    #define p_data    p_hdr.ph_data
    #define p_len     p_hdr.ph_len
    #define p_type    p_hdr.ph_type
    #define p_flags   p_hdr.ph_flags
    #define p_dat     p_databuf

pbuf's must be allocated and deallocated using the routines p_get() and p_free() declared in 
$PDIR/include/pbuf.h. Since a pbuf contains less than 512 bytes of data (PHLEN is defined as 512 minus header length), an MTU-sized packet (1500 bytes in your projects) will consist of 4 pbuf structures linked together by the p_next field in each pbuf -- this is called a pbuf chain. The p_nextpkt field can be used to link multiple packets together on a queue. By convention, only the first pbuf in a pbuf chain should be used to link to another pbuf chain (throughp_nextpkt).

Figure 3: A 48-byte IP packet spread out over 2 pbuf structures. There is a 20-byte IP header, an 8-byte UDP header, and 20-bytes of user data. The IP header starts at the beginning of the first pbuf's p_databuf, while the UDP header and data bytes start in the middle of the second pbuf's p_databuf. Placing data in the middle of p_databuf and modifyingp_data to point to it is a clever way to leave space for headers, or to push and pop headers, without requiring additional pbufs.

\includegraphics[height=2.5in, keepaspectratio]{fig-pbufs.eps}

The field p_data points to the location where the packet data starts within the p_databuf[PHLEN] buffer. Why implement pbufs this way? Suppose your transport layer has built a UDP packet with 20 bytes of data and an 8-byte UDP header. Before this packet gets sent on the wire, it will have to go through netowrk and link layer processing. If you place the data at the beginning of the pbuf, the network layer will have to allocate a new pbuf in which to store the 20-byte IP header and prepend this pbuf to the packet. However, if you were clever enough to leave 20 bytes of space at the beginning of the p_databuf[] buffer, you could simply subtract 20 from the value of p_data and then copy the 20-byte IP header to the address indicated by this pointer. An example of a packet consisting of multiple pbuf structures is shown in Figure 3.

The field p_len is the length of data contained in the pbuf; it is not the total length of the packet. p_type is managed by the pbuf allocation code and p_flags is presently not used at all by the kernel.

In addition to the functions p_get() and p_free() mentioned above, there are some other functions that you may find useful for manipulating pbuf's. They are defined in $PDIR/include/pbuf.h. Some examples of these are:

  • p_pktlen(): Returns the total length of a packet.
  • p_freep(): Frees all pbuf's of a packet.
  • p_copyp(): Makes a copy of a packet.


5 Interacting with the link layer

In your projects you will be adding a network layer to the simulator. The network layer transmits and receives packets from the network with the help of the link layer. In this section, we describe how to do this.

5.1 The network interface list

The link layer at each node is intialized by the simulated kernel at boot time. The kernel boot code reads the network configuration file (Section 2.1) and creates a list of networking interfaces on the node. Each element on this list is a struct ifnet defined in $PDIR/include/if.h:

  struct ifnet {
    TAILQ_ENTRY(ifnet)      if_next;

    int                     if_index;       /* interface number */
    struct sockaddr_in      if_addr;        /* address of interface */
    struct sockaddr_in      if_netmask;     /* netmask of if_addr */
    int                     if_mtu;         /* MTU of interface */

    void (*if_start)(struct ifnet *ifp, struct pbuf *p);

    struct hwif             *if_hwif;       /* hardware device */
  };

The head of this list can be accessed by calling the function ifnet_listhead() provided by the simulator. The TAILQ_ENTRY() macro is a macro defined in$PDIR/include/queue.h that is useful for creating linked lists. Iterating over the interface list can be done as follows:

  struct ifnet *ifp = ifnet_listhead();

  for( ; ifp; ifp = TAILQ_NEXT(ifp, if_next)) {
    printf(``interface index: %d\n'', ifp->if_index);
  }

5.2 Handing packets to the network interface for transmission

Once your IP layer has completely built a packet and has determined which interface the packet should be sent out on, the IP layer can send this packet by calling the if_start() routine of the appropriate interface. Note that if_start() will free the pbuf of a packet after transmitting the packet. For example, if your routing table indicates that a packet should go out interface 1, you would do the following:

  struct ifnet *ifp;
  struct pbuf *p;            /* packet to be sent */

  /* ifp = code to find interface 1 here */

  ifp->if_start(ifp, p);     /* send the packet */


5.3 Getting packets received by the network interface

When a network interface receives a packet from the network, it copies the packet from its own internal buffer into a pbuf data structure (or a pbuf chain if the packet is larger than a pbuf) in main memory. The interface then calls your ip_input() routine which is the entry point into the network layer. The device knows it should call this function because the function is registered as the entry point into the network layer during kernel initialization.


6 The Socket API

The socket layer provides an API (application program interface) for user programs to access the networking functionality of the kernel. In project 1, you wrote your FTP server using the socket API provided by the Solaris kernel, for example, socket()bind()accept(), etc. These calls are ``system calls'' provided by the kernel so that user programs can use kernel functionalities.

For user programs to interface to the simulator, you can use the following socket calls: Socket()Close()Bind()Read()Write()Sendto()Recvfrom(), andSetsockopt(). Their prototypes are defined in $PDIR/include/Socket.h (this header file should be included by user programs, not your kernel). There are two important issues regarding these calls:

  • Observe that the first letter of each call is capitalized. This is to distinguish them from the actual Solaris system calls, which will go into the Solaris kernel upon invocation. All your user programs will be linked against a library provided by us so that when they invoke these capitalized calls, the corresponding handlers in our simulated kernel (not Solaris) are called.
  • These calls meet the standard specifications, as described by the man pages of the ``lower-case'' versions on a Solaris machine or by Stevens' network programming book [1]. However, there are some exceptions (for example, added flags for some calls) which will be described in the remainder of this section.


6.0.1 The Socket() call

The Socket() call accepts three arguments: familytype, and protocol. It supports the following three combinations of family and type:

  1. AF_INET/SOCK_STREAM: this combination specifies that the user wants to create a TCP socket. The following system calls are allowed on a TCP socket:Close()Bind()Accept()Connect()Write()Read(), and Setsockopt(). There is no Listen() call, its functionality is subsumed by the Accept() call as described in Section 7

  2. AF_INET/SOCK_DGRAM: this combination specifies that the user wants to create a UDP socket. The following system calls are allowed on a UDP socket:Close()Bind()Sendto()Recvfrom(), and Setsockopt().
  3. AF_INET/SOCK_ICMP: this combination specifies that the user wants to create an ICMP socket. The following system calls are allowed on an ICMP socket:Close() and Recvfrom().
  4. AF_ROUTE/SOCK_RAW: this combination specifies that the user wants to create a routing socket. The following system calls are allowed on a routing socket: Close()Write(), and Setsockopt().

6.0.2 The Recvfrom() call

Here is the prototype of the Recvfrom() system call:

int Recvfrom(int s, void *buf, int len, int flags, struct sockaddr *from, 
int *fromlen);

The arguments to this call are basically the same as the standard socket call. The Recvfrom() call reads ``one packet at a time''. It returns the length of the message written to the buffer pointed to by the buf argument (the second argument). Even if one packet worth of message does not ``fill up'' the buffer,Recvfrom() will return immediately and will not read the second packet. However, if a message in a packet is too long to fit in the supplied buffer, the excess bytes are discarded.

By default, Recvfrom() is blocking: when a process issues a Recvfrom() that cannot be completed immediately (because there is no packet), the process is put to sleep waiting for a packet to arrive at the socket. Therefore, a call to Recvfrom() will return immediately only if a packet is available on the socket.

When the argument flags of Recvfrom() is set to MSG_NOBLOCKRecvfrom() does not block if there is no data to be read, but returns immediately with a return value of 0 bytes. MSG_NOBLOCK is defined in $PDIR/include/systm.h. In an actual UNIX system, socket descriptors are set to be non-blocking using fcntl() with type O_NONBLOCK, and Recvfrom() returns errno EWOULDBLOCK when there is no data to be read on the non-blocking socket.

6.0.3 The Sendto() call

The Sendto() call has an argument flags, which is ignored by the current implementation.

6.0.4 The Write() call

As you can see in Section 6.0.1 , the Write() call cannot be used with a UDP socket. This is quite different from the standard write() call in UNIX, which can be used with any socket. The Write() call can only be used with a routing or a TCP socket.

6.0.5 The Setsockopt() call

The Setsockopt() call has five arguments. The arguments level and optname specify the option that you want to set. The simulator supports only theIPPROTO_IP level and the IP_FW_SETIP_NAT_SET, and IP_IF_SET options (defined in $PDIR/include/Socket.h). The option value for the IP_IF_SET option needs to be a pointer to a struct if_info (defined in $PDIR/include/route.h). The data structures of the option values for IP_FW_SET and IP_NAT_SET have not been defined.


7 TCP

Typically, there are two types of applications that use TCP sockets - servers and clients. A TCP server listens on a well-known port (or IP address and port pair) and accepts connections from TCP clients. A TCP client initiates a connection request to a TCP server in order to setup a connection with the server. A real TCP server can accept multiple connections on a socket. A server socket in the simulator accepts only one TCP connection in its lifetime.

Below is the sequence of socket calls made by a TCP server and a TCP client:

Server: Socket -> Bind -> Accept -> Read/Write -> Close
Client: Socket -> (Bind) -> Connect -> Read/Write -> Close

Below are the details of the socket calls specification.

  • A server must call Bind() in order to bind the socket to a port, before calling Accept(). Otherwise, Accept() returns an error. Bind() to a client socket is optional.

  • We make the following changes to the Accept() specification:

    1. Accept() returns 0 on success (instead of a new file descriptor), and -1 on failure. Accept() does not create a new file descriptor (unlike the Berkeley Socket specification), and uses the same file descriptor for all subsequent socket calls.

    2. The socket starts accepting client connection requests only after the Accept() call has been made (i.e. packets arriving before the Accept() call are discarded).

    3. If during the connection establishment phase the protocol times out (e.g. during the TCP three-way handshake) or an error occurs, Accept()waits for other connection attempts, and returns only when a connection has been established successfully.
    4. Accept() is always blocking--Accept() should block until after a connection establishment is completed.

  • Connect() is always blocking. It returns 0 if the connection establishment (TCP three-way handshake) succeeds, and returns -1 if an error occurs. One possible error is a timeout during the three-way handshake. In all cases where errors occur during the Connect() call, an application should not callConnect() again but should call Close() immediately.

  • Read() is always blocking. On success, the number of bytes read is returned. If Read() returns 0, this indicates an ``End of File'', meaning that the other side has closed the connection, and no more data will be received by this socket.

  • Write() returns almost immediately, except when the send buffer is full. This buffer is used by TCP to keep all data that could not be sent immediately (because of window limitations), as well as, data that has been sent but has not yet been acknowledged. If the send buffer is full, Write() blocks until the send buffer is freed to enqueue another packet.

  • When a user program calls Close() on a TCP socket, the socket is marked as closed and the Close() function returns to the process immediately. The socket descriptor is no longer usable by the process, i.e. it would not be usable as an argument to Read() or Write(). TCP tries to send all data that is already queued to be sent to the other end, and after this occurs the normal TCP connection termination sequence takes place [1].

8 Special IP addresses

8.1 The IP address INADDR_ANY

When you wrote your simple FTP server in project 1, you probably bound your listening socket to the special IP address INADDR_ANY. This allowed your program to work without knowing the IP address of the machine it was running on, or, in the case of a machine with multiple network interfaces, it allowed your server to receive packets destined to any of the interfaces. In reality, the semantics of INADDR_ANY are more complex and involved.

In the simulator, INADDR_ANY has the following semantics: When receiving, a socket bound to this address receives packets from all interfaces. For example, suppose that a host has interfaces 0, 1 and 2. If a UDP socket on this host is bound using INADDR_ANY and udp port 8000, then the socket will receive all packets for port 8000 that arrive on interfaces 0, 1, or 2. If a second socket attempts to Bind to port 8000 on interface 1, the Bind will fail since the first socket already ``owns'' that port/interface.

When sending, a socket bound with INADDR_ANY binds to the default IP address, which is that of the lowest-numbered interface.

8.2 The IP address INADDR_BROADCAST

The kernel picks the UDP or TCP socket to which a packet sent to the INADDR_BROADCAST address (255.255.255.255) is delivered in the following way: If there is a socket that is bound to the address assigned to the interface from which the packet was received, the packet will be delivered to this socket. If there is no such socket, the packet will be delivered to one of the sockets bound to INADDR_ANY. Obviously, the destination port of the packet and the port to which the socket was bound to need to match in both cases.


9 Routing Sockets

Figure 4: An example of how a user process could add a route to the kernel's routing table.
\begin{figure}\hrule {\scriptsize\begin{verbatim}int s;
char buf[1024];
str...
...msglen) < 0) {
perror(''Write'');
exit(1);
}\end{verbatim}}\hrule\end{figure}

Routing sockets are created with domain AF_ROUTE and type SOCK_RAW. This type of socket is used with the following system calls: Socket()Close()Write(), and Setsockopt(). A routing socket is a special type of socket that is not specific to any particular network protocol, but allows ``privileged'' user processes to write information into the kernel. User processes use this type of socket to add and remove information from the routing table. This is done by filling in the rt_msghdr structure and passing it to Write(). The rt_msghdr structure is defined in $PDIR/include/route.h. Figure 9 shows example code for modifying the route table.

The following values of the rtm_type field of the rt_msghdr structure are supported:

  • RTM_ADD: add an entry to the routing table;
  • RTM_DELETE: delete an entry from the routing table;
  • RTM_CHANGE: change an entry in the routing table. The implementation of this command is equivalent to performing an RTM_DELETE followed by an RTM_ADD.

These constants are defined in $PDIR/include/route.h.

To look up a route within the kernel, call rt_lookup_dest() (defined in $PDIR/include/rtable.h). This function will return the address of the gateway to which a packet with the given destination address should be sent. It also returns the index of the interface over which the packet should be sent. Note that the current implementation of the simulator supports only host routes, there is no support for network masks or prefix matching. If there is no host route available in the routing table, the function returns the default gateway. The default gateway route is defined as a route that has a destination address of 0.0.0.0. It can be inserted into the routing table in the same way as host routes.


10 ICMP

ICMP is an integral part of any IP implementation. ICMP is normally used to communicate error messages between IP nodes (both routers and endhosts), but it is occasionally used by user-level applications such as traceroute. If you are not familiar with ICMP, you should consult RFC 792 [2] and/or Stevens'TCP/IP Illustrated, Volume 1 [3].

An ICMP message can be either a query message or an error message, and it has a type field and a code field. To send an ICMP message within the kernel, call icmp_send() (defined in $PDIR/include/icmp.h) and pass it the packet causing the error condition and the desired type and code.

There is also a mechanism for a user-level process to read ICMP packets received by a host. In a real UNIX socket implementation, a process opens a RAW socket to receive ICMP packets. In the simulator, there is a new socket type SOCK_ICMP, which is defined in $PDIR/include/systm.h. You can open an ICMP socket as follows:

Socket(AF_INET, SOCK_ICMP, 0);

The ICMP socket implementation also supports the Recvfrom() and Close() socket calls. Note that the ICMP header of a received packet is not stripped by the kernel so that a user-level process can access the ICMP header to see the error type and code, among other things.

Another important issue is that there is no need to bind an ICMP socket to a particular IP address: the ICMP socket will get ICMP messages received by any of the host's interface(s) (similar to the use of INADDR_ANY). As a result, you should make sure that at most one ICMP socket is opened at any given time. If an ICMP packet arrives at a host, and no ICMP socket is opened, the host drops the packet.

11 Internet checksum: the in_cksum() function

Checksum computation is one of the operations that dominate packet processing time. Efficient checksum computation is difficult to implement since it is hardware dependent. Therefore, an operating system kernel usually implements several machine-dependent versions of the Internet checksum functionin_cksum() to be used on different platforms. To simplify your task in projects 2 and 3, we provide you with a portable C version of the in_cksum() function (see $PDIR/include/in_cksum.h). This version is from the BSD TCP/IP implementation (though modified to use our pbuf structure, instead of the BSD mbuf).in_cksum() calculates the checksum of the packet specified by the first argument (a pointer to pbuf) with length specified by the second argument. You can use this function to compute all checksums in projects 2 and 3.

Bibliography

1
W. Richard Stevens. 
UNIX Network Programming Volume 1, Networking APIs: Sockets and XTI
Prentice Hall, second edition, 1997.

2
J. Postel. 
Internet Control Message Protocol. 
RFC 792, USC/Information Sciences Institute, September 1981.

3
W. Richard Stevens. 
TCP/IP Illustrated, Volume 1: The Protocols
Addison-Wesley, 1994.

About this document ...

Simulation Environment Overview 
15-441 Project 2 and 3, Fall 2001

This document was generated using the LaTeX2HTML translator Version 99.2beta8 (1.43)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds. 
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were: 
latex2html -mkdir -dir htmlsim_single -image_type gif -link 3 -toc_depth 3 -split 3 -nonavigation -noaddress -antialias -white -notransparent -show_section_numbers simulator.tex

The translation was initiated by Urs Hengartner on 2001-10-02






15-441 Project 2, Fall 2001 
IP Layer and Extensions 

Out: Tuesday, Oct 2, 2001 
Due: Thursday, Oct 25, 2001, 5:00 pm


1 Introduction

In project 1 we asked you to implement a simple FTP server using the networking functionality provided by the Solaris kernel, so by now you are familiar with the usage of network functions such as socket()bind()listen()accept(), etc. In this project, your task is to implement the IP layer of the network stack. You will link your IP layer to our simulator, which implements the basic pieces of an operating system as well as the physical, link, transport, and socket layers of the simulated network.

The resulting system is very realistic, and will provide you with useful experience in network programming. Details of the simulator and how to use it are described in a separate handout.

When implementing the IP layer, you will also have to add support for network address translation (NAT) and for stateless firewalling.

You will also implement the Dynamic Host Configuration Protocol (DHCP), which allows a host to acquire a dynamic IP address.

In project 3, you will add stateful firewalling to the IP layer implemented in this project.

2 Overview

In this section we present a high-level overview of the project. We will go into more detail in later sections. The networking code in the kernel is organized into several layers which you have already seen in class. The organization of the network code in the simulated kernel is shown in Figure 1. Note that the application layer has been broken into two ``mini'' layers, the first one consisting of the actual user programs and the second one of the socket layer.

Figure 1: Networking code organization in the simulated kernel

\includegraphics[height=2in, keepaspectratio]{fig-comp.eps}

  • In order to access the kernel's networking functionality, user programs use the socket API, which includes system calls such as Socket(). In this project, you will implement a DHCP client and server application that use the socket layer. DHCP allows a client host that does not have a static IP address assigned to it to acquire a dynamic IP address from the server host.

  • The socket layer is a protocol-independent interface to the transport protocol. It is already implemented in the simulator. The functionality provided by the socket layer is described in the simulator handout.

  • The transport layer contains the implementation of the transport protocols UDP and TCP. These protocols are already implemented in the simulator.

  • The network layer contains an implementation of the IP protocol. For this project, you will implement basic IP functionality of a router, that is, input and output processing of datagrams and forwarding of datagrams. In addition, you will also add support for NAT and firewalling.

  • The link layer and the physical layer are implemented in the simulator. Your network layer will interact with the link layer to send/receive packets. The functionality provided by the link layer is described in the simulator handout.

The project directory for this project will be:

/afs/cs.cmu.edu/academic/class/15441-f01/projects/project2/

In this handout, we will use $PDIR to denote this directory. For your/our convenience, we provide you a template for some of the code that you must write. This includes a Makefile, skeleton definitions of some important structures, skeleton prototypes of some interface functions, etc. In this handout, we will reference the names of some functions/structures defined in the template files. All the template files are in $PDIR/template.

3 The Network Layer: IP

The IP layer provides a common interface between different transport-layer protocols and different link-layer devices. The network layer for this project will be modeled after IPv4 [1]. You can also read Chapter 3 of Stevens' TCP/IP Illustrated, Volume 1 [2] or Chapter 4 of the textbook, since they are probably easier to read than the RFC.

Your implementation does not need to handle IP fragmentation, multicast, IP header options, and type-of-service (ToS). Therefore, your fragment offset field should always be zero. The identification field is used to uniquely identify each IP packet sent by a host. It is typically used for fragmentation and reassembly. Although we do not ask you to implement fragmentation, you should set the identification field according to the specification. A simple heuristic such as ``incrementing by one each time a packet is sent'' is sufficient for our purposes.

You are, however, responsible to correctly set, handle, and verify IP version number, IP header length, IP packet length, time to live (TTL), protocol number, checksum, and source and destination addresses.

Prototypes of the IP functions that you need to implement are provided in $PDIR/template/ip_io.c. When a node boots, it calls ip_init(). If your implementation of the IP layer requires initialization code, you should place it there.


3.1 Sending Packets

Figure 2: A user program sends a packet by Sendto()

\includegraphics[height=3in, keepaspectratio]{fig-send.eps}

This section describes what happens when a user program sends a packet using the UDP transport protocol. The steps look similar when using the TCP transport protocol. Figure 2 depicts the functions involved. First, the user program calls Sendto() to send a packet. The simulated kernel invokes the corresponding socket layer handler k_sendto().

One important task of k_sendto() is to convert the data (passed in by the user using the pointer argument) into a pbuf data structure (or a chain of pbuf's). This is because, inside your kernel, a packet is passed around using the pbuf structure. The pbuf structure is very similar to the mbuf structure used in real BSD-style networking stacks. It is described in detail in the simulator handout. For now, just remember that, inside the kernel, a packet is represented by a pointer to a pbuf.

After verifying arguments and converting the data into pbuf's, k_sendto() invokes the UDP-layer interface function udp_usrreq_send(), which invokes the UDP function udp_send()udp_send() is the function that actually performs the send operation of the UDP layer, for example, adding the UDP header, doing the checksum, etc. There are corresponding functions for TCP.

After udp_send() finishes its work, ip_output() is invoked. ip_output() will construct the IP header, calculate the checksum, query the routing table (by callingrt_lookup_dest()) to find the output interface, and so on. When ip_output() finishes constructing the packet, it invokes the link-layer function if_start() on the appropriate interface, which then sends out the packet. In this figure, if_start() is provided by our simulated kernel, and you will implement the functionality provided by ip_output(). Details of how to interact with the link layer are described in the simulator handout.

The output function ip_output() is passed a pointer to a pbuf, which contains the data packet to be sent. You can assume that space for the IP header has already been allocated in this pbuf and that the p_data member of the passed pbuf structure points to this IP header.


3.2 Receiving Packets

Figure 3: Receiving a packet from the simulated network

\includegraphics[height=3in, keepaspectratio]{fig-recv.eps}

This section describes what happens when a packet is received from the simulated network. Figure 3 depicts the sequence of steps that take place. After a packet is received from the network by the network interface device, and processed by the link layer, the processed packet is delivered to the network layer. More specifically, the link layer will invoke the IP routine ip_input() (more details on how to interact with the link layer can be found in the simulator handout).

When ip_input() is called, it will check the packet for errors, such as IP version mismatch, IP checksum error, etc. If the packet is error-free, there are two possible scenarios:

  1. The packet is destined to this host: In this case, ip_input() will deliver the packet to UDP, TCP, or ICMP. This is done by invoking udp_receive(),tcp_receive(), or icmp_receive(), depending on the type of the packet. (Note that although icmp_receive() is at the transport layer in the figure, ICMP is not really a transport protocol.). If tcp_receive() or udp_receive() fail because there is no receiver binding to the port the packet was sent to, an ICMP message of type ICMP_UNREACH and code ICMP_UNREACH_PORT needs to be sent back to the sender by the network layer. This failure condition can be detected by checking the return value of tcp_receive() or udp_receive(), which is going to be set to FAILURE_PORT_UNREACH. To avoid ICMP floods, ICMP messages must not be sent as a reaction to failed icmp_receive() calls. Interaction with ICMP is described in the simulator handout.

  2. The packet is destined to some other host: Since the destination is not us, IP needs to forward the packet to its destination. Forwarding a packet to its destination is called ``routing''. Routing is typically done only by routers and not by end hosts. To forward a packet, ip_forward() is invoked, which performs some additional work such as decrementing the IP TTL. It then calls ip_output() to actually send out the packet (ip_output() is described in Section 3.1). If the TTL in an IP packet expires, an ICMP message of type ICMP_TIMXCEED and code ICMP_TIMXCEED_INTRANS needs to be sent back to the sender.

To complete our description of what happens when a packet is received, let's look at what takes place above the network layer, when the packet is a UDP packet. In this case, udp_receive() will process the packet (checking for errors, etc.) and then invoke the socket layer function enqueue_data() to insert the packet into the socket receive buffer of the socket this packet is addressed to.

The socket layer function enqueue_data() then inserts the packet into the socket receive buffer of the specified socket structure. The enqueue_data() function must wake up any processes blocked on the Recvfrom() call on the socket. Recvfrom() grabs a packet from the socket receive buffer and return immediately only if the buffer is not empty. If it is empty, the process that called Recvfrom() must be blocked, waiting for the arrival of packets. Therefore, whenenqueue_data() is called, it must check whether there is any process blocked on Recvfrom(), and if there is, it must wake up the blocked process(es). ThenRecvfrom() can grab the packet and return immediately to the user process.

3.3 Handling Broadcast Packets

Your IP layer should be able to handle broadcast packets (since the DHCP protocol relies on them). More specificially, it needs to provide support for IP packets sent to the limited broadcast address 255.255.255.255. Your implementation should follow these rules:

  • Datagrams destined to this address must never be forwarded by a router.

  • Locally generated datagrams with this destination address must be sent out over all interfaces.

  • Received datagrams with this destination address must be forwarded to the transport layer. However, unreachable ports must not result in ICMP messages.

4 Firewalling

Figure 4: Processing of a packet in the IP layer.

\includegraphics[height=2in, keepaspectratio]{fig-iplayer.eps}

For this project, you should implement a simple firewall based on packet filtering. In packet filtering, packets are dropped when they fulfill certain criteria. Figure 4 shows the processing of packets in the IP layer. A packet originates either from the link layer or it is generated by a local process. As can be seen from the figure, filtering can take place at three places: packets destined to the local host go through input filtering, packets received from another host and destined to a remote machine go through forward filtering, and locally generated packets go through output filtering.

4.1 Filter Rules

Your firewall should drop packets based on criteria like the protocol of a packet or its destination address. The default policy is to accept a packet. Explicit rules specify which packets should be dropped.

The syntax of a rule looks as follows:

<queue>: <protocol> <src address> <src port> -> <dst address> <dst_port>  \
         (<rule options>)

For example, the rule

input: tcp 1.2.3.4 any -> 5.6.7.8 80 (flags: SF; ttl: 30)
specifies that all TCP packets from any port on 1.2.3.4 to port 80 on 5.6.7.8 should be dropped in input filtering, if they have the SYN and FIN flag set and a TTL value of 30.

The keyword any can be given for addresses, ports, and the protocol. For port numbers, a range of port numbers is permitted. For example, 80:100 will match on ports between 80 and 100.

Possible values for <queue> are inputoutput, or forward. The value specifies the queue to which a filter should be applied.

Possible values for <protocol> are tcpudp, and icmp.

Rule options are optional. They match on particular fields in the IP or TCP header. The exact header layout can be retrieved from RFC 791 [1] and RFC 793 [3], respectively. All rule options are separated from each other using the semicolon ";" character. Rule option keywords are separated from their arguments with a colon ":" character.

The following rule options apply to fields in the IP header:

ttl: <number>;Time to Live
id: <number>;Identification
dsize [>|<] <number>;Total Length (The greater/less than signs can be used to indicate ranges
 and are optional.)

The following rule options apply to fields in the TCP header:

seq: <number>;Sequence Number
ack: <number>;Acknowledgment Number
flags: <flag values>;TCP Flags

The flags rule tests the TCP flags for a match. There are six flags variables: S (SYN), F (FIN), R (RST), P (PSH), A (ACK), and U (URG). There are also logical operators that can be used to specify matching criteria for the indicated flags: + (ALL flag, match on all specified flags plus any others), * (ANY flag, match on any of the specified flags), and ! (NOT flag, match if the specified flags are not set in the packet). For example, flags: SF* matches on all packets that have at least either the SYN or FIN bit set. There can be at most one logical operator. If no logical operator is given, there has to be an exact match between the flags in the packet and the flags specified in the filter.

4.2 Rules Instantiation

To instantiate the rules discussed in the previous section, you will use an existing mechanism in the simulator for passing information from the user layer to the network layer. This mechanism is based on the function Setsockopt(). You should write a user program setfilter that parses a file containing filter rules and that configures the firewall through Setsockopt() calls. The only argument to the program is the name of a file containing filter rules (e.g., setfilter -n 1 rules.txt). To add a filter to the firewall from a user program, you need to call Setsockopt() on a routing socket with level IPPROTO_IP and option nameIP_FW_SET. Routing sockets are described in the simulator handout. The fourth argument to Setsockopt() is a pointer to an arbitrary data structure, whose length is given in the fifth argument. You should come up with a convenient data structure for passing filter rules and use it as the fourth argument toSetsockopt().

Given the level/option name combination mentioned above, the kernel will call fw_setsockopt(), where the actual configuration is done. You need to provide the body of this function, that is, parsing its arguments, whereas one of them is the data structure you defined, and instantiating the filtering rules.

Prototypes of the firewalling functions that you need to implement are provided in $PDIR/template/firewall.c. When a node boots, it calls fw_init(). If your firewalling implementation requires initialization code, you should place it there.

5 Network Address Translation

With Network Address Translation (NAT), IP addresses are mapped from one realm to another, in an attempt to provide transparent routing to hosts. Traditionally, NAT devices are used to connect an isolated address realm with private unregistered addresses to an external realm with globally unique registered addresses. There are many variations of address translation. For the project, you are going to implement Basic NAT [4].

Figure 5: Network Address Translation.

\includegraphics[height=2in, keepaspectratio]{fig-nat.eps}

Figure 5 demonstrates the usage of Basic NAT in an example scenario. Hosts A and B have private IP addresses 191.68.0.1 and 192.168.0.5, respectively. Private IP addresses are unique within the local network, but not globally. All the packets going from the private network to the Internet have to go through a NAT box. The NAT box is assigned two globally unique IP addresses (145.67.8.3 and 145.67.8.4) for address translation. Assume host A wants to communicate with a host in the Internet that has the globally unique address 198.76.34.2. It generates a packet with this destination address. The source address is set to its private address. When the NAT box receives this packet, it replaces the private source address with the global address 145.67.8.3. Similarly, if host B wants to communicate with a host on the Internet, the NAT box uses 145.67.8.4 as source address for packets coming from B. For IP packets on the return path, a similar address translation is required.

Figure 4 shows where in the IP layer NAT takes place. For this project, we assume that the NAT box has a globally unique IP address for each host in the private network and that the mapping between a private and a global address is static. In your implementation, you need to implement Basic NAT. You do not need to change port numbers in TCP/UDP packets and you do not need to deal with ICMP packets. However, in addition to the source address (or the destination address for the reverse path), it might be required to also modify some other fields in the TCP/UDP/IP header to make end-to-end communication work.

You should write a user program setnat that parses a file containing the mappings from private to public IP addresses. An example entry is given below:

192.168.0.1 -> 128.2.4.5

The only argument to the program is the name of a file containing the mappings (e.g., setnat -n 1 mapping.txt). To add a mapping to the NAT box from a user program, you need to call Setsockopt() on a routing socket with level IPPROTO_IP and option name IP_NAT_SET. It is up to you to come up with a convenient data structure for the option value. Given this level/option name combination, the kernel will call nat_setsockopt(), where the actual configuration is done. You need to provide the body of this function.

Prototypes of the NAT functions that you need to implement are provided in $PDIR/template/nat.c. When a node boots, it calls nat_init(). If your firewalling implementation requires initialization code, you should place it there.

6 Dynamic Host Configuration Protocol

The Dynamic Host Configuration Protocol (DHCP)  [5] provides configuration parameters to Internet hosts. DHCP consists of two components: a protocol for delivering host-specific configuration parameters from a DHCP server to a host and a mechanism for allocation of network addresses to hosts. DHCP is built on a client-server model, where designated DHCP server hosts allocate network addresses and deliver configuration parameters to dynamically configured hosts. In this project, you should implement a DCHP client and a DHCP server.

Figure 6: DHCP.

\includegraphics[height=2in, keepaspectratio]{fig-dhcp.eps}

Figure 6 gives a brief overview of the DHCP protocol. A host that does not have a static IP address assigned to it invokes the DHCP client. The client broadcasts a DHCP discover msg. A or multiple DHCP servers on the same network receive this message and broadcast a DCHP offer message. The message contains the IP address that is offered to the client by a server and further configuration parameters such as the IP address of a router. The client picks one of the offered IP addresses and broadcasts a DHCP request message to let the DHCP servers know of its choice. Finally, the DHCP server whose IP address got picked by the client confirms this choice with a DHCP ack packet.

Typically, a dynamic IP address is assigned a finite lease, allowing for serial reassignment of network addresses to different clients. If a lease expires, a client should renew its lease or get a new IP address. If a client no longer needs its address, it should issue a message back to the server to release it.

Both the DHCP client and DHCP server are user-level applications. Your DHCP server should be called dhcp_server and have the following interface:

dhcp_server --topology=FILE --config=FILE --default_lease=LENGTH --max_lease=LENGTH

The topology file contains the topology of your network. This file is identical to the one you need for starting up the simulator. You need to pass this file toread_config() (defined in $PDIR/include/rconfig.h). The configuration file lists each interface of the server that supports DHCP. Also, for each interface, it contains the IP address that should be handed out on that interface. An example configuration file is given below:

1 192.168.0.1
2 192.168.0.5

The fact that only one address can be handed out on an interface is due to a limitation of the simulator. Typically, a DHCP server connected to an Ethernet can hand out multiple IP addresses to different hosts on the interface to the Ethernet. However, the simulator does not provide Ethernet support. It supports only direct links between hosts. Therefore, there can be only one client host per interface that requests an address from the DHCP server.

The default lease argument gives the default lease time of addresses handed out to clients and the maximum lease argument their maximum length.

Your DHCP client should be called dhcp_client and have the following interface:

dhcp_client --lease_length=LENGTH --release_time=LENGTH

The lease length gives the suggested length of the lease. A server should return a shorter length if the suggested length is longer than the maximum lease length. The release time indicates after how many seconds a client should release its assigned IP address. If the release time is after the expiration time of a lease, the client needs to renew the lease before.

The DHCP protocol is specified in RFC 2131 [5]. DHCP options are specified in RFC 2132 [6]. Your protocol should support at least the DHCPDISCOVER, DHCPOFFER, DHCPREQUEST, DHCPACK, and DHCPRELEASE messages. Some more requirements/suggestions:

  • The simulator does not support hardware addresses. Therefore, the chaddr field in DHCP messages should always be zero and the DHCPDISCOVER and DHCPREQUEST messages should contain a client identifier option. The client identifier needs to be unique per client. We suggest that you use the node number of a client for this purpose. The function get_node_number() (defined in $PDIR/include/rconfig.h) returns this number.

  • Nodes in the simulator can receive only messages that are sent to their destination address or to the broadcast address 255.255.255.255. Therefore, a client should set the BROADCAST flag in DHCPDISCOVER and DHCPREQUEST messages.

  • Neither the server nor the client should probe the network before allocating an address and receiving an address, respectively.

  • A client can immediately accept an offered address and does not have to wait for other offers.

  • You do not need to handle reboots of clients/servers.

  • In DHCPOFFER and DHCPACK messages, the server should return a router option that is set to the IP address of the interface over which the server received the DHCP request. The client should use this address as default gateway for routing. Information about the default gateway of a node is available in the simulator handout.

  • The server should be able to handle DHCP requests from multiple interfaces. Note that you are not allowed to create new processes or threads.

  • The client and server should be able to deal with lost DHCP messages.

You can set the IP address of an interface using Setsockopt(). The socket must be a routing socket, the level is IPPROTO_IP, the option name IP_IF_SET, and the option value a pointer to struct if_info (defined in $PDIR/include/route.h).

7 Logistics

7.1 Groups

This project is to be done in groups of two. Send an email to uhengart+441@cs.cmu.edu listing the names of the two group members and their Andrew user IDs as soon as possible. The body of the email should look as follows:

member1     <andrew user ID>        <member name>
member2     <andrew user ID>        <member name>

For example,

member1     uh        Urs Hengartner
member2     rbalan    Rajesh K. Balan

You are not permitted to share code of any kind with other groups, but you may help each other debug code. Each member of the group is responsible for both sharing the work equally, and for studying the work of their partner to the point they understand it and can explain it unassisted.

7.2 Equipment and tools

For this project (and all subsequent projects in the course) you will use computers from Sun Microsystems running the Solaris operating system. Such machines are available in several clusters, notably the Wean Hall clusters. You may work from any workstation you have access to, be it a Solaris machine or not, by telnetting to unixXX.andrew.cmu.edu. The support code for this project uses the Solaris threads package internally, so you will not be able to compile, test or run your network stacks except on Solaris machines.

Since your partner and you will both work on the project, it is essential that you make sure you are not modifying the same file at the same time! Therefore, we highly recommend that you use some form of version control, such as RCS or CVS. You can find a brief tutorial for RCS on the project web page. It also includes pointers to information regarding CVS.

All programs must be written in either C or C++. Note that the TAs do not provide support for problems occuring due to the usage of C++. We strongly recommend you compile your code with gcc and debug it with gdb. The template Makefile is already set up to use gcc. Also strongly recommended is the use of the -Wall and -Werror flags for gcc. This will force you to get rid of all possible (well, almost) sources of easily avoidable bugs (so that you can concentrate your debugging efforts on the unavoidable ones). Again, the template Makefile is already set up with these flags.

7.3 Provided Code

All of the provided code and infrastructure is available in $PDIR, which has the following contents:

  • README: a file in the project2 directory describing the contents;
  • include/: the header files for your interface with the simulator -- DO NOT copy or modify these!!;
  • lib/: the archive files containing the simulator that you link to;
  • template/: a template for the code you must write. It also includes a Makefile, the startkernel.pl script, etc. -- you should copy all these files to your own working directory;
  • utils/: a collection of utilities for testing and configuring your network, explained in the README file in this directory.

It is very important that you do not copy the libraries that we provide into your own directory. You should link against the library where it resides in the official project2 directory. This is already taken care of in the template Makefile that we provide. We reserve the right to make changes to the library as needed, and you will not receive the updates if you link against your own copy.

The code in the $PDIR/template/ directory is merely a suggestion: you are free to modify any of the code there or start from scratch. However, it will make it easier for us to help you if you have used the template and retain the function prototypes that we have suggested. Also, the template was designed with a C implementation in mind. It may be more difficult to follow this template if you intend to use C++ to implement your networking layer.

Also note that your project code will use both the Solaris standard header files and the header files specific to this project. The header files such as<netinet/*.h> or <sys/*.h> are the standard header files on typical UNIX machines. <project2/include/*.h> and "*.h" are header files specific to this project. When including a header file, you should be conscious whether the header file is a standard header file or a project-specific file.

FYI, you don't need to define data structures for the various headers. They are already defined in several standard Solaris header files. Several examples:

  • The UDP header: struct udphdr in <netinet/udp.h>.
  • The ICMP header: struct icmp in <netinet/ip_icmp.h>.
  • The IP header: struct ip in <netinet/ip.h>.
There are some other header files in the /usr/include/netinet/ directory that you may find useful. However, you probably don't need all of them. In fact, you don't need to use these structures at all if you prefer defining your own. We just want to let you know that they exist.

7.4 Communication

We reserve the right to change the support code as the project progresses to fix bugs (not that there will be any :-) and to introduce new features that will help you debug your code. You are responsible for reading the bboards and the project home page to stay up-to-date on these changes. We will assume that all students in the class will read and be aware of any information posted to the bboards and the project home page.

If you have any question regarding this project, please post your question to the class bboard. Please make your questions clear and specific to increase the chance that we can solve your problem with one response. As always, the course staff is available for help during office hours.

7.5 A Brief Report

Each group should create a brief report describing their efforts, in one of the following formats: plain text, postscript, or html. Please use the file nameproj2_report.{txt|ps|html}. Your report should describe the following:

  • A breakdown of what each group member did (use a table for this).
  • At a high-level, describe your implementation of the firewalling support and the DHCP protocol.
  • Describe any interesting testing strategies that you have used.
  • Describe what works and what does not, and if not, why (use a table for this as well).
  • Your thoughts on the project: was anything too difficult? What would improve the project?

7.6 Submission and Policy

You must use a Makefile, and your project should build completely by typing gmake. We will post further guidelines for how to submit your project and report.

Note that we do not expect to grant any extensions for this project. We must be notified immediately of any extenuating circumstances you might have, including problems with your partner. A disaster that occurs during the last few days before the deadline may NOT qualify you for an extension, since you still would have had the majority of the project duration to work. For example, don't come to us complaining that your partner has been bad for two weeks or you have been sick for two weeks and you need an extension. If you need help with this sort of problem, you must come to us before the damage is irreparable.

7.7 Grading

We will post the grading scheme for the project. The grading philosophy for this project is that your grade increases with the number of components that work. It is better to have a few solidly-working components than many slightly-broken components, and this should influence your implementation plan.

8 Advice

It is critical to realize that the third project in this course will require you to extend the IP layer implemented in this project to include additional functionality. This makes a clean design and well-written code a must, since you will live with the code you write now for the next 2 months. If you make a mess of this project, you will cause yourself no end of pain and agony when you attempt to complete project 3. Therefore, when you are designing data structures, you should make sure that your code can be easily extended.

While each student has a different set of commitments and will have to manage his or her time accordingly, this project is a significant effort, so you will have to start early and make steady progress throughout the next three weeks.

We suggest that you first familiarize yourself with the simulated kernel. Read the simulator handout, compile the provided dummy sources of the IP layer, and start the simulator. Then, implement the IP layer and test it thoroughly for different topologies. Next, add NAT and firewalling to the IP layer. Finally, you implement the DHCP server and client. The NAT/firewalling and DHCP implementation are very independent, making it easy to split the work between group members. The design of all the parts should be done jointly, that is, we expect each student to be familiar with all the pieces.

Bibliography

1
J. Postel. 
Internet Protocol. 
RFC 791, USC/Information Sciences Institute, September 1981.

2
W. Richard Stevens. 
TCP/IP Illustrated, Volume 1: The Protocols
Addison-Wesley, 1994.

3
J. Postel. 
Transmission Control Protocol. 
RFC 793, USC/Information Sciences Institute, September 1981.

4
P. Srisuresh and K. Egevang. 
Traditional IP Network Address Translator (Traditional NAT). 
RFC 3022, Intel Corporation, January 2001.

5
R. Droms. 
Dynamic Host Configuration Protocol). 
RFC 2131, Bucknell University, March 1997.

6
R. Droms. 
DHCP Options and BOOTP Vendor Extensions). 
RFC 2132, Silicion Graphics Inc, Bucknell University, March 1997.

About this document ...

15-441 Project 2, Fall 2001 
IP Layer and Extensions 

Out: Tuesday, Oct 2, 2001 
Due: Thursday, Oct 25, 2001, 5:00 pm

This document was generated using the LaTeX2HTML translator Version 99.2beta8 (1.43)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds. 
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were: 
latex2html -mkdir -dir htmlproj2_single -image_type gif -link 3 -toc_depth 3 -split 3 -nonavigation -noaddress -antialias -white -notransparent -show_section_numbers proj2.tex

The translation was initiated by Urs Hengartner on 2001-10-02






기법 소개 :  http://www.linux.co.kr/security/certcc/IP_Network_Scanning_Skill.pdf
ping(ICMP / RAW socket 예제) : http://blog.hwport.com/106


SSL Certificates HOWTO : http://wiki.kldp.org/HOWTO/html/SSL-Certificates-HOWTO/index.html

보안서버 SSL이란? :  http://blog.daum.net/crosscert/4

SSL에 대한 일반적 이해 :  http://www.certkorea.co.kr/doc.asp?t_crypt

OpenSSL 을 이용한 인증서 생성 : http://jmnote.com/wiki/%EA%B0%9C%EC%9D%B8%EC%84%9C%EB%AA%85_SSL_%EC%9D%B8%EC%A6%9D%EC%84%9C_%EC%83%9D%EC%84%B1

OpenSSL로 인증서 생성 및 변환 간략 정리 : http://golmong.tistory.com/112


1.OpenSSL_암호화_프로그래밍_첫걸음.pdf

2.OpenSSL_API를_이용한_비밀키_암호화,_MD_프로.pdf

3.OpenSSL_API를_이용한_공개키_암호화.pdf

4.OpenSSL_API를_이용한_인증서,_SSL_프로그래밍.pdf

OpenSSL_인증서발부법.ppt


출처 : http://www.ibm.com/developerworks/kr/library/ac-iscssl1/index.html


인터넷 같은 오픈 통신 네트워크를 통한 데이터 보안은 개발자와 사용자들에게는 큰 걱정거리이다. 따라서, 여러분이 사용하고 있는 제품에 보안 환경을 설정하는 것이 매우 중요하다.

Netscape Communications와 RSA Data Security가 합동으로 개발한 Secure Sockets Layer (SSL)는 효율적인 방식으로 이러한 보안을 이룩할 수 있다. SSL은 암호화, 인증서 기반 인증, 구축된 네트워크 연결을 통한 보안 협상을 제공하고, 많은 기업들과 제품들은 자신들의 통신 프로토콜에 SSL을 채택하고 있다.

본 시리즈에서는, 다음 두 가지 주제를 집중 조명하기로 한다:

  1. 상세한 SSL 작동 방법
  2. Integrated Solutions Console Versions 5.1과 Versions 6.0.1 환경에서 SSL이 실행되는 방법

이 글에서는 SSL을 연구하고, 이것이 왜 Integrated Solutions Console 환경에 구현되어야 하는지 그 이유를 설명한다. 본 시리즈 Part 2와 Part 3에서는, Integrated Solutions Console Versions 5.1과 Versions 6.0.1에서 SSL을 구현하여 실행하는 단계별 가이드를 제공한다.

먼저, SSL이란 무엇인가?

SSL이란 무엇인가?

SSL은 TCP/IP를 사용하는 두 개의 통신 애플리케이션 간 프라이버시와 무결성을 제공하는 프로토콜이다. 클라이언트와 서버를 오고 가는 데이터는 시메트릭 알고리즘을 사용하여 암호화 된다.

퍼블릭 키 알고리즘(RSA)는 암호 키들의 교환과 디지털 서명에 사용된다. 퍼블릭 키 암호는 메시지를 암호화 하는데 사용되는 두 개의 키들을 사용하는 알고리즘을 정의한다. 하나의 키가 메시지를 암호화 하는데 사용되면, 다른 키는 암호 해제에 사용된다. 하나의 키(퍼블릭 키)를 공개하고 다른 키(개인 키)는 숨겨서 안전한 메시지를 받을 수 있다.

디지털 인증서

SSL에서 중요한 역할을 하는 디지털 인증서에 대해 알아보자. 디지털 인증서는 두 가지 목적을 갖고 있다:

  • 소유자의 신원 확인
  • 소유자가 퍼블릭 키를 사용할 수 있도록 함

디지털 인증서는 신용 기구 -- 인증서 기구 (CA) -- 에서 발행하며, 제한된 시간 동안에만 발행된다. 만료일이 지나면, 디지털 인증서는 교체되어야 한다. SSL은 키 교환, 서버 인증, 클라이언트 인증에 디지털 인증서를 사용한다.

디지털 인증서에는 인증서 소유자의 신분과 인증서 기구에 대한 정보가 포함되어 있다:

  • 소유자의 이름.
  • 소유자의 퍼블릭 키.
  • 디지털 인증서가 발행되었던 날짜.
  • 디지털 인증서가 종료하는 날짜.
  • 발행 기구의 이름이다. CA의 기구명이다.
  • 발행 기구의 디지털 서명.

SSL 연결은 http:// 대신 https://로 시작하는 URL을 사용하여 클라이언트에 의해 시작된다.

SSL 인증서 유형

SSL은 인증서를 사용하여 연결을 확인한다. 이러한 SSL 인증서는 보안 서버에 놓이고, 데이터를 암호화 하고 웹 사이트를 확인하는데 사용된다. SSL 인증서는 그 사람이 속해 있는 사이트를 확인하고, 인증서 보유자에 대한 정보, 인증서가 발행되었던 도메인, 인증서를 발행했던 Certificate Authority의 이름을 포함하고 있다.

다음은 SSL 인증서를 얻을 수 있는 세 가지 방법이다:

  1. Certificate Authority(CA) 인증서를 사용한다.
  2. 자가 서명 인증서를 사용한다.
  3. 더미 인증서를 사용한다.

Certificate Authority(CA) 인증서 사용하기

Certificate Authority는 업계의 신임을 받는 기구이며, 인터넷 인증서를 발행하고 있다. 대표적인 예로 VeriSign을 들 수 있다. CA 서명 인증서를 획득하려면, 충분한 정보를 CA에 제공하여 CA가 여러분의 신원을 확인할 수 있도록 해야 한다. CA는 새로운 인증서를 만들고, 이것을 디지털 서명을 한 다음, 여러분에게 제공한다. 대중적인 웹 브라우저들은 특정 CA에 의해 서명된 인증서를 신임하도록 미리 설정되어 있다. 클라이언트가 SSL을 통해 인증서가 발행된 서버로 연결하기 위해 추가적인 클라이언트 설정이 필요 없다.

자체 서명 인증서 사용하기

자가 서명 인증서는 사용자가 생성한 인증서이다. 자가 서명 인증서를 사용할 때, 인증서 발행자는 주제와 같다. 이 솔루션의 강점은 CA 서명 서버 인증서를 획득하는 것보다 자가 서명 서버 인증서를 만드는 시간이 덜 걸린다는 점이다. 하지만, 자가 서명 인증서는 SSL 연결을 통해 인증서를 설치하는 서버로 연결된 클라이언트가 인증서의 서명자를 신임하도록 설정되어야 한다. 인증서는 자가 서명되었기 때문에, 서명은 클라이언트의 트러스트 파일에 있지 않으므로, 추가되어야 한다. 모든 클라이언트의 트러스트 파일에 액세스 하는 것이 비현실적이라면, 이러한 설정을 사용하지 않는다. 대신 CA 서명 인증서를 얻어야 한다. 자가 서명 인증서는 서버와 인터랙팅 하는 클라이언트가 인증서를 신임하도록 설정될 수 있을 때에만 유용하다.

더미(dummy) 인증서 사용하기

멍청이(dummy) 인증서를 뜻하는 것이 아니다. 일반적으로 더미 인증서에는 지정된 환경에 SSL을 설정하고 그 기능을 테스트하는데 임시적으로 사용될 수 있는 플레이스홀더로서 작동하는 "가상의" 정보를 포함하고 있다. Integrated Solutions Console은 더미 인증서와 서버 및 클라이언트 트러스트와 키 파일을 제공한다.

그러면 이제, 인증서를 얻은 후에는 어떻게 할까?

클라이언트/서버 인증

인증서를 획득한 후에는 인증을 받아야 한다. 두 가지 유형의 SSL 인증이 있다:

  • 서버 측 인증
  • 클라이언트 측 인증

SSL 서버 인증은 서버의 신원을 확인할 수 있도록 한다. SSL을 실행하는 클라이언트 소프트웨어는 퍼블릭 키 암호라는 표준 기술을 사용하여 서버의 인증서와 퍼블릭 ID가 유효하고, 신임을 받는 CA의 클라이언트 리스트에 있는 인증서 기구에서 발행된 것인지를 검사한다. 만약 사용자가 네트워크를 통해서 신용 카드 번호를 보내고, 서버가 이를 받았는지 검사하고 싶을 경우 이와 같은 확인은 중요하다.

SSL 클라이언트 인증은 사용자의 신원을 서버가 확인할 수 있도록 한다. 서버 인증에 사용되었던 것과 같은 기술을 사용하여, SSL을 실행하는 서버 소프트웨어는 클라이언트의 인증서와 퍼블릭 ID가 유효하고, 신임을 받는 CA의 클라이언트 리스트에 있는 인증서 기구에서 발행된 것인지를 확인한다. 만약 서버가 기밀 금융 정보를 고객에게 보내고 있는 은행이고, 수신자 신원을 확인해야 하는 경우 이러한 확인은 중요하다.

그림 1은 이러한 프로세스를 시각적으로 묘사한 다이어그램이다:


그림 1. 클라이언트/서버 인증
The client/server authentication dance

키 파일(Key file) 대 트러스트 파일(Trust file)

WebSphere® Application Server에 의해 사용되는 SSL은 SSL 키 파일에는 개인 인증서를, 트러스트 파일에는 서명자의 인증서를 저장한다. 키 파일에는 인증서 모음이 포함되어 있는데, 각각 신원을 확인하기 위해 SSL 연결 초기화 동안 제공된다. 트러스트 파일에는 믿을 수 있는 것으로 간주된 인증서 컬렉션이 포함되어 있고, 제공된 인증서는 SSL 연결 초기화 동안 매치되어 신원을 확인한다.

SSL과 WebSphere Application Server

SSL 구현의 좋은 예는 IBM® WebSphere Application Server에 있다. 층을 이룬(layered) 보안 아키텍처에 보안이 구축된다. (그림 2)


그림 2. WebSphere Application Server의 보안 레이어
WebSphere Application Server의 보안 레이어

Network Security 레이어는 전송 레벨 인증과 메시지 무결성과 암호를 제공한다. 분리된 WebSphere Application Server 서버들간 통신은 SSL과 HTTPS를 사용하도록 설정될 수 있다. 또한, IP Security와 Virtual Private Network (VPN)는 메시지 보호에 사용될 수 있다.

SSL 프로토콜은 전송-레이어 보안-인증, 무결성, 기밀성-을 WebSphere Application Server의 클라이언트와 서버 간 보안 연결에 제공한다. 이 프로토콜은 TCP/IP 위, HTTP, LDAP, IIOP 같은 애플리케이션 프로토콜 밑에서 실행되며, 전송 데이터에 트러스트와 프라이버시를 제공한다. 클라이언트와 서버의 보안 설정에 따라서, 다양한 레벨의 트러스트, 데이터 무결성, 프라이버시가 확립된다. SSL의 기본 연산을 이해하는 것이 올바른 설정에 매우 중요하며, 클라이언트와 애플리케이션 데이터에 바람직한 보호 레벨을 확립할 수 있다.

다음은 SSL에서 제공하는 일부 보안 기능들이다:

  • 데이터 암호는 데이터가 와이어를 통해 흘러가는 동안 민감한 정보가 노출되는 것을 방지한다.
  • 데이터 서명은 데이터가 와이어를 통해 흘러가는 동안 수정되는 것을 방지한다
  • 클라이언트와 서버 인증은 올바른 사람 또는 머신과 통신하고 있다는 것을 확인해 준다.

SSL은 엔터프라이즈 환경에서 효과적인 보안이 될 수 있다.

SSL은 WebSphere Application Server 내에서 다중 컴포넌트에 의해 사용되어 트러스트와 프라이버시를 제공한다. 이러한 컴포넌트들은 HTTP 전송, ORB, 보안 LDAP 클라이언트다.

WebSphere Application Server에서 사용되는 SSL 구현은 IBM Java™ Secure Sockets Extension (IBM JSSE) 또는 IBM System SSL이다. IBM JSSE 프로바이더에는 SSL과 Transport Layer Security (TLS) 프로토콜을 지원하는 레퍼런스 구현과 애플리케이션 프로그래밍 인터페이스(IP) 프레임웍이 포함되어 있다. IBM JSSE 프로바이더에는 Java 2 플랫폼의 서명 관련 Java Cryptography Architecture (JCA) 기능을 위한 RSA 지원, 일반 SSL과 TLS cipher 수트, X.509 기반 키와 트러스트 매니저, JCA 키스토어 인증서용 PKCS12 구현을 제공하는 표준 프로바이더가 포함되어 있다.

JSSE 프로바이더 설정은 대부분의 다른 SSL 구현(예를 들어, GSKit) 설정과 비슷하다. 두 가지 차이점을 주목하라:

  • JSSE 프로바이더는 SSL 키 파일의 서명자와 개인 인증서 스토리지를 지원하지만, 트러스트 파일이라고 하는 개별 파일도 지원한다. 트러스트 파일에는 서명자 인증서만 포함될 수도 있다. SSL 키 파일에 모든 개인 인증서를 두고, 서명자 인증서는 트러스트 파일에 둔다. 개인 인증서만 보유할 수 있을 정도의 메모리를 가진 저렴한 하드웨어 암호 장치가 있을 경우에 이 같은 방식이 적당하다. 이 경우, 키 파일은 하드웨어 장치를 의미하고, 트러스트 파일은 서명자 인증서의 모든 것을 포함하고 있는 디스크 상의 파일을 의미한다.
  • JSSE 프로바이더는 플러그인에 의해 사용되는 상용 SSL 키 파일 포맷((.kdb 파일)을 인식하지 않는다. 대신, JSSE 프로바이더는 Java Key Store (JKS) 같은 표준 파일 포맷을 인식한다. SSL 키 파일은 플러그인과 애플리케이션 서버 간 공유되지 않는다. 더욱이, 키 관리 유틸리티의 다른 구현은 애플리케이션 서버 키와 트러스트 파일을 관리하는데 사용되어야 한다.

SSL과 Integrated Solutions Console

Integrated Solutions Console은 콘솔 모듈을 호스팅 및 통합할 수 있는 일반적인 웹 기반 관리 콘솔 프레임웍을 제공하여, 사용자들은 특정 IBM 제품들 보다는 솔루션을 관리한다. 이 프레임웍에는 포틀릿 컨테이너, 자바 관리 애플리케이션, Eclipse Help 모듈이 포함되어 있다.

SSL은 기밀성과 암호를 제공하도록 설정될 수 있다. 클라이언트 브라우저와 Integrated Solutions Console 서버 간 통신은 SSL을 사용하여 보호된다. Integrated Solutions Console이 폼 기반 인증을 사용하고, 이것은 로그인 동안 전송되는 사용자 ID와 패스워드를 암호화 하지 않기 때문에 암호화는 중요하다. 콘솔 모듈이 보안 연결을 통해 백엔드 리소스에 액세스 해야 한다면, 포틀릿은 SSL을 사용할 수 있다.

SSL이 중요한 이유?

왜 이것이 문제가 되는가? 오픈 통신 채널을 통해 안전하게(효과적으로) 데이터를 전송하는 것은 현대적인 IT 시스템을 관리하는데 있어 필수적인 요소이기 때문에, SSL은 이러한 보안을 확립하는데 도움이 되는 강력한 프로토콜이고, Integrated Solutions Console 환경에서 SSL을 실행하는 것은 복잡하고 도전이 되는 태스크가 될 수 있다. 왜 도전이 되는가? Integrated Solutions Console 같은 웹 기반 애플리케이션 환경에서의 데이터 보안은 초보자에게는 약간 모호해 보인다. IT 보안 자체가 광범위한 주제이고, 오픈 통신 네트워크에서 다양한 측면들을 다루고 있기 때문이다.

나머지 기술자료에서는 Integrated Solutions Console 기반 환경에서 SSL 중심의 데이터 보안을 설명하도록 하겠다. Integrated Solutions Condole 5.1 (더미, 자가 서명, CA 인증서 포함)용 SSL의 설정과 실행을 설명한 다음, Integrated Solutions Console 6.0.1에 똑 같은 것을 설정하는 방법을 설명하겠다.


참고자료

교육

제품 및 기술 얻기

출처 : http://blog.pages.kr/59


스위치의 기능 및 동작 방식


# 스위치는 IEEE 802.1D 의 규정에 따라 동작하는데 트랜스패런트 브리징과 스패닝 트리 프로토콜이 있다.

# STP는 2대 이상의 스위치가 복수개의 링크로 연결되어 있을 때 이중성을 유지하면서 루프가 발생하지 않도록 하는 방식이다.

# 트랜스패런트 브리징은 스위치가 MAC 주소 테이블을 만들고 유지하며 이를 참조하여 수신 프레임을 목적지 포트로 전송하는 방식. 스위치는 MAC 주소 테이블을 보고 프레임을 스위칭한다

# 리피터 

 - 전기적인 신호만 증폭시켜 먼 거리까지 도달 할 수 있도록 한다.

# 허브

 - 리티어뫄 마찬가지로 전기적인 신호를 증폭시켜 LAN의 전송거리를 연장시키고 여러 장비를 LAN에 접속할 수 있도록 해준다.

 - LAN 에서 사용하는 CAT 5는 전송거리가 100m이다. 허브를 연결하면 전송거리를 연장할 수 있다.

 - 프레임을 수신하면 연결된 모든 장비로 플임을 Flooding 한다.

 - PC가 많을수록 충돌위험도 많고 Half Duplex 로만 동작한다.

 - 허브는 하나의 Collision Domain 이다.

# 브리지

 - 포트는 보통 4개 이하이고, 속도가 느리다. 확장된 개념은 스위치

 - 신호를 증폭시키는 것이 아니라 Frame 을 재생한다. 즉 수신한 Frame 을 다시 만들어 전송한다.

 - MAC 주소를 보고 포트를 결정하므로 레이어 2장비라고 한다.

# 스위치

 - MAC 주소 테이블을 가지고 있고 목적지 MAC 주소를 가진 장비가 연결된 포트로만 Frame 을 전송한다.

 - 각각의 포트가 하나의 Collison Domain 이다. 즉 충돌이 감소

 - 스위치에 연결된 모든 장비는 하나의 Broadcast Domain 이다.

 - 스위치에 연결된 장비가 많을수록 Broadcast 트래픽도 증가해 네트워크 성능저하가 심각해 진다.

 - 해결책 VLAN 구성. 같은 스위치도 VLAN 이 다르면 Broadcast 프레임을 차단한다.

# 라우터

 - 네트워크 주소가 다른 장비들을 연결해준다.

 - VLAN 이 서로 다른 장비들간의 통신은 라우터를 통해야만 가능

 -  WAN 구간의 장거리 통신이 가능하다.

# L3 스위치

 - 라우터의 속도 한계를 극복하고 VLAN 간의 고속 라우팅을 지원

 - VLAN 포트간에 스위칭 기능을 제공, 다른 VLAN 포트간에는 라우팅 기능을 제공한다.

 - 라우터보다 라우팅 속도가 빠르지만 장거리 통신망을 연결하는 포트가 없다.

# L2 스위치의 동작 원리

 - 이더넷에 전송되는 트래픽은 모두 MAC 주소를 기반으로 동작함 스위치는 입력된 트래픽을 처리할때 MAC 주소를 참조해  처리

 - 어떤 포트에서 트래픽이 수신되면 출발/목적지 MAC 주소를 확인해 목적지 MAC 주소는 어떤 포트로 보낼 것인지 결정, 출발지 MAC

   주소는 해당 MAC 주소가 있는 포트를 기억해 다른 포트에 해당 MAC 주소를 찾는 트래픽이 들어오면 그 포트로 보내는데 사용한다.

 - MAC Address Table을 사용한다.

# L3 스위치의 동작 원리

 - 3계층 스위치는 입력된 패킷의 목적지 IP 를 보고 어떤 포트로 전송할 것인가를 결정한다.

 - Routing Table 이라는 것을 기본적으로 가지고 있으며 이 정보를 참조해 트래픽을 처리한다.

1) Dynamic Routing Table

 - 대형 네트워크에서 모든 장비의 라우팅 테이블을 하나씩 관리하기는 매우 어렵다. 때문에 장비들간에 정보를 주고받을 수 있도록 자동으로 라우팅 정보를 관리하는 기능

2) Source Routing

 - 보통 프레임을 네트웍을 경유할때 각 라우터들은 목적지 IP를 확인하고 다음 경유지를 선택한다.

 - 소스 라우팅은 프레임을 보내는 쪽에서 라우터의 도움 없이 어떤 경로로 가야하는지 설정해 주는 것이다.

3) Routing Cache

 - 라우팅을 처리하는 장비들은 자신이 라우팅 테이블을 참조해 처리한 패킷의 경로를 기억한다. 이를 라우팅 캐시라고 한다.

 - 패킷이 입력되면 부하가 많은 라우팅 테이블을 매번 참조하지 않고  메모리에 있는 라우팅 캐시 테이블에서 해당 패킷 정보가 있는지 먼저 확인한다. 이정보는 일정시간 동안 참조되지 않으면 삭제한다.

# Transparent Bridging

 - 라우터가 라우팅 테이블을 참조해 패킷을 라우팅 시키는 것처럼

 - 스위치는 스위칭 테이블, 즉 MAC 주소 테이블을 보고 패킷을 스위칭 시킨다.

 - 이처럼 테이블을 만들어 이를 참조해 프레임을 스위칭하는 방식을 트랜스패런트 브리징이라고 한다.

 - Learning : 이더넷 프레임을 수신하면 스위치는 자신의 MAC 주소를 확인한다. 이때 발신지 MAC 주소가 없으면 수신 포트와번호와 발신지 MAC 주소를 자신의 MAC 주소 테이블에 기록한다.

 - Aging : MAC 주소 테이블에 해당 주소가 있으면 에이징 타이머를 리셋시킨다. 기본 5분. 이 시간이 경과하면  제거한다.

 - Flooding : 목적지 주소가 Broadcast 주소이거나 자신의 MAC 주소 테이블에 발신지 MAC 주소가 없을 경우 수신포트를 제외하고 모든 포트로 해당 프레임을 전송

 - Filtering : 프레임을 수신한 포트로 다시 송신되지 않게 포트를 차단한다. 

- Forwarding : 목적지 주소가 MAC테이블에 존재하는 유니캐스트 프레임을 수신하면 목적지로 프레임을 전송한다.


스위치 기본 설정 및 동작 확인

# IOS 도움말 기능은 물음표 (?) 를 이용한 명렁어 안내와 화면에 에러 메시지를 표시해 주는 것으로 구분할 수 있다.

 설정 커맨드 창에서 ? 를 입력하면 현재 단계에서 사용할 수 있는 모든 명령어를 표시해 준다.

 

# 이용자 모드 : 스위치의 모드에는 이용자 모드, 관리자 모드, 설정모드로 구분할 수 있다. 이용자 모드는 처음 스위치에 접속 했을때 들어가는 모드이며, 기본적인 정보확인 및 기본테스트만 가능하다.

 

# 관리자 모드 : 이용자 모드에서 『 enable 』명령어를 사용하여 들어갈 수 있다. 관리자 모드는 show 명령어를 이용한 정보확인 debug, copy, telnet, ping 등의 명령어를 사용할 수 있다.

『 en 』 을 입력한후 TAB 키를 누르면 명령어가 완성된다. 또한 ? 를 입력하면 en 으로 시작하는 명령어를 볼 수 있다.

 

# 설정모드 : 관리자 모드에서 Configure Terminal 을 입력하며 들어갈 수 있는데 통상 줄여서 Conf t 를 입력한다. 설정모드에서는 관리자용 패스워드 및 스위치의 이름 등 스위치 관련 설정이 시작되는 모드이다.

 

# 설정시 유용한 팁

1. 리플래시 : 명령어를 입력하는 도중 인터페이스의 상태가 변하면 상태변화를 알려주는 메시지와 명령어가 섞이는데 이를 방지해준다.

   Switch(config)# line console 0
   Switch(config-line)# logging synchronous <- 줄여서 logging sync

 

2. 콘솔 대기 시간 삭제 : 일정시간이 지나도 설정창이 대기모드로 돌아가는걸 방지해준다.

   Switch(config)# line console 0
   Switch(config-line)# exec-timeout 0 0

 

3. DNS 서버 요청 단계 삭제 : 오타를 입력했을 때 장비가 DNS 서버를 찾는 시간을 줄일 수 있다.

   Switch(config)# no ip domain-lookup

 

# IP 부여하기 : L2 스위치에  IP 를 부여하는 목적은 원격 접속으로 스위치를 관리하기 위해서이다. IP 를 부여하기 위해서는 VLAN 인터페이스 설정모드로 들어가야 한다.

  Switch(config)#  int vlan 1

  Switch(config)#  ip add 17.17.1.1 255.255.255.0

  Switch(config)#  no shutdown

  

# 스위치 접속을 실시할때 패스워드 단계 설정

1. 콘솔(Console) 패스워드 설정

  Switch(config)# line console 0
  Switch(config-line)# password cisco
  Switch(config-line)# login
  Switch(config-line)# exit

 

2. 옥스(Aux) 패스워드 설정

  Switch(config)# line aux 0

  Switch(config-line)# password cisco
  Switch(config-line)# login
  Switch(config-line)# exit

 

3. 텔넷(VTY) 패스워드 설정 <=  패스워드 설정을 하지 않으면, 원격지에 텔넷 접속이 불가능하다.

  Switch(config)# line vty 0 15
  Switch(config-line)# password ciscotel
  Switch(config-line)# login

 

4. 스위치 패스워드 설정

   secret 는 패스워드를 암호화 되어서 저장한다. password 는 패스워드가 암호화 되지 않는다. secret, password 둘다 설정했을 경우 패스워드는 secret 로 변경된다.

  Switch(config)# enable secret cisco1
  Switch(config)# enable password cisco2

이럴경우 스위치 패스워드는 cisco1 로 등록된다.

 

# 모든 패스워드를 암호화하는 방법

  Switch(config)# service password-encryption

 

# 전체 설정내용 확인 : 현재 적용된 전체 설정을 보기 위해서는 show running-config (줄여서 show run) 을 사용하면 된다.

 설정파일의 크기, OS 버전, 스위치 이름, 인터페이스의 설정내용, 패스워드 설정등을 볼 수 있다.

  Switch#  show run

 

# 인터페이스 설정내용 확인 : 특정 인터페이스에 대한 설정을 확인하려면 show run interface 명령어를 사용하면 된다.

  Switch#  show running-config interface FastEthernet0/1   <= 줄여서 show run int fa0/1

 

# 인터페이스의 IP 정보확인 : 인터페이스에 설정된 IP 주소와 레이어 1, 2계층의 상태를 확인하려면 show ip int brief 를 사용한다.

 Switch#  show ip int brief

 Interface                     IP-Address         OK?       Method       Status      Protocol

  VLAN1                       unassigned        YES       unset         down        down

  VLAN2                       17.17.1.1             YES       manual        up            up

  FastEthernet0/1          17.17.2.1             YES       manual        up            up



트랜스패런트 브리징 및 ARP 동작과정

# 트랜스패런트 브리징은 스위치파트의 기초라고도 할 수 있지만 아주 중요한 부분이다. 반드시 동작과정을 이해 해야만 한다.

 

# 통신에서 트랜스패런트(투명한) 라는 용어가 자주 나온다. 그럼 왜 투명한가? 눈으로 확인을 할 수 없기 때문이다. 너무나 빨라서일 수 도 있고 디버깅으로도 확인을 할 수 없다. 하지만 동작과정을 암기가 아닌 이해를 하고 있어야 한다.

 

# 트랜스패런트 브리징 : 라우터가 라우팅 테이블을 참조하는 것 처럼 스위치는 스위칭 테이블을 확인해 프레임을 스위칭 한다.

 이더넷 스위치가 MAC 주소 테이블을 만들고, 유지.. 이를 참조해 프레임을 스위칭하는 방식을 말한다.

 

# 트랜스패런트 동작과정 : 스위치는 프레임을 수신하면 제일 먼저 프레임의 출발지 MAC 주소를 확인하는 작업을 하게 된다.

 이때 자신의 MAC 주소 테이블에 출발지 MAC 주소가 존재한다면 Aging 타임을 리셋 시킨다. Aging 타임의 기본값은 300초이다. 만약 300초동안 자신이 가지고 있는 MAC 주소의 프레임을 수신하지 못한다면 스위치는 MAC 주소 테이블에서 해당 MAC 주소를 삭제 하게 된다. 이러한 이유는 스위치는 MAC 주소를 무한정 가지고 있을 수 없기 때문이다.

 출발지 MAC 주소가 자신의 MAC 주소 테이블에 없다면 다음 단계로 Learning 을 하게 된다.

 즉 학습을 하는 것인데 출발지 MAC 주소와 해당 포트를 자신의 MAC 주소 테이블에 기록을 하는 과정을 말한다.

 다음으로 스위치는 목적지 MAC 주소를 확인한다. 목적지 MAC 주소 테이블에 해당 프레임의 목적 MAC 주소가 존재할 경우 스위치는 송 / 수신 포트를 확인을 한다. 만약 송 / 수신 포트가 동일하다면 스위치는 프레임을 차단하는데 이를 Filtering 이라 한다. 만약 송 / 수신 포트가 일치 하지 않는다면 스위치는 유니캐스트로 프레임을 Forwarding 하게 된다.

 목적지 MAC 주소가 자신의 MAC 주소 테이블에 존재 하지 않을경우 스위치는 수신한 포트를 제외한 모든 포트로 브로드캐스팅을 날리게 되는데 이를 Flooding 이라고 한다.

 

# ARP 동작 과정 : L2 계층 장비인 스위치는 데이터 통신을 하기 위해서는 필수조건으로 MAC 주소를 알아야만 통신이 가능하다.

 목적지 MAC 주소를 모를 경우 통신이 불가능하다는 말이다. 하지만 그렇다고 통신을 안할 수도 없는 노릇이다.

 이러한 상황에 사용할 수 있는 프로토콜이 ARP(Address Resolution Protocol) 이다. ARP 는 목적지 IP 주소를 이용해 목적지 MAC 주소를 찾아 통신이 이루어지도록 동작한다.

 

# PC1이 PC2에게 데이터를 전송하려고 하는데 PC2 의 MAC 주소를 모른다고 할 경우 PC1은 목적지 MAC 주소를 브로드캐스트 주소로 하는 ARP를 요청하게 된다.

 

# 이때 출발지 MAC 주소는 자신의 MAC 주소를 사용하고 목적지 IP는 PC2의 IP를 출발지 IP는 자신의 IP를 포함한 프레임을 보내게 되는 것이다.

 

# 스위치에는 10대의 PC가 물려 있다고 한다면 ARP 요청 프레임을 수신한 스위치는 PC1에 해당하는 포트를 제외한 나머지 9개의 포트로 ARP 요청 프레임을 전송하게 되는 것이다.

 

# 하지만 PC2에 해당하는 포트를 제외한 나머지 포트는 자신에게 온 메세지가 아니란것을 확인후 해당 프레임을 드롭시킨다.

 

# PC2는 자신에게 온 프레임이란걸 확인하고 PC1에게 자신의 MAC주소를 유니캐스트로 전송한다. PC1은 유니캐스트 프레임을 수신후 목적지 MAC 주소를 이용해 PC2와 데이터 통신을 할 수 있게 되는 것이다.

 

# 서로다른 네트워크에 속한 PC의 ARP 동작 과정을 살펴보면 위에서 설명한 비슷하게 동작하게 된다. 하지만 L3 장비는 브로드캐스트 프레임을 차단시키는 기능을 사용한다.

 

# PC1은 자신이 속한 세그먼트에 목적지 MAC 주소가 없다는걸 확인한후 Default Gateway 로 설정된 주소로 ARP를 요청한다.

 

# L3장비 (라우터 혹은 L3 스위치) 는 프레임을 수신하고 자신의 테이블에 목적지 MAC 주소가 없다는걸 확인하면 Default Gateway로 ARP 프레임을 송신하는데 이때 출발지 MAC 주소는 PC1의 MAC 주소가 아닌 L3장비 (라우터 혹은 L3 스위치) 자신의 MAC 주소로 변경해서 송신하게 된다.

 

# ARP 프레임을 수신한 PC2는 자신의 MAC 주소를 유니캐스트로 전송하게 되는 것이다.



VLAN, 트렁킹 및 VTP

# VLAN이란 논리적으로 분할된 스위치 네트워크를 뜻하는데 VLAN을 설정하는 이유는 브로드캐스트를 나눠 네트워크의 성능저하를 방지하고 보안성을 강화하기 위해서 사용한다.

# VLAN 의 장점 : LAN 구간 이더넷 환경에서 증가되는 브로드캐스트 트래픽의 플러딩 범위를 최소화하여 브로드캐스트양을 줄인다.

 서로 다른 VLAN 은 다른 브로드캐스트 도메인이기 때문에 접근이 차단되며 보안을 유지할 수 있다.

 관리상 효율적이며, 이동성이 강하기 때문에 장소의 제한이 없다.

 

# 브로드캐스트 도메인이 커진다는 뜻은 브로드캐스트 트래픽이 많이 발생해 장비 및 네트워크의 성능을 저하 시킨다는 말이다. 스위치에 연결된 PC가 브로드캐스트 프레임을 전송하면 모든 스위치의 모든 포트로 이 프레임이 플러딩되지만 논리적으로 VLAN 을 나누면 이런 브로드캐스트 프레임을 플러딩 하지 않고 차단한다.

 

# 서로다른 VLAN에 접속된 장비들은 상호간에 통신을 하기 위해서는 L3 장비를 통해야만 통신이 가능하다.

 

# DTP : DTP는 스위치끼리 트렁크와 관련된 내용을 주고 받을 때 사용하는 프로토콜이다. 스위치 끼리 연결 되어있다면 DTP 모드를 설정 하기위해 서로 DTP 프레임을 주고 받게 된다. DTP 모드에는 4가지가 있는데 Access, Trunk, Desirable, Auto 이다. 이중 실제로 동작하는 것은 Access 와 Trunk 두가지이다.

 

# Access 모드 : 자신의 인터페이스에 스위치 포트를 Access 모드로 설정하면 상대방 포트와 상관없이 Access 모드로 동작한다.

   Switch1(config)#  int fa1/0

   Switch1(config)#  switchport mode access

 

# Trunk 모드 : Trunk 모드로 설정하려면 Encapsulation 방식을 지정해야 하는데 Encapsulation 방식에는 dot1q, isl, negotiate 3가지 방식이 있다. 여기서 기억해야 할건 Trunk 로 연결하려면 양쪽 포트가 같은 Encapsulation 방식을 사용해야 한다.

 아래와 같이 Encapsulation 방식을 정하고 Trunk 모드로 설정하면 상대방 포트와 상관없이 Trunk 모드로 동작한다.

   Switch1(config)#  int fa1/0

   Switch1(config)#  switchport trunk encapsulation dot1q (혹은 isl)

   Switch1(config)#  switchport mode trunk

 

# IEEE 802.1q는 표준 프로토콜, ISL은 Cisco 전용 프로토콜이다.

 

# Desirable 모드 : 스위치포트의 디폴트 DTP모드가 바로 Dynamic Desirable이다. Dynamic Desirable은 상대방 포트에서 DTP 프레임을 받아 상대방 포트모드에 따라 자신의 모드가 결정되어 지는 것이다. 예를들어 서로 연결된 2개의 포트가 모두 디폴트 Dynamic Desirable로 동작하고 있을때 한쪽의 DTP 모드를 Access로 바꾸면 나머지 한쪽도 Access로 변경된다.

 또한 한쪽포트가 Trunk모드로 설정되었다면 나머지 한쪽도 Trunk 모드로 동작하게 된다. 뿐만 아니라 Desirable, Auto 모드로 설정되어도 Trunk 모드로 동작하게 된다.

                      

# 쉽게 말하자면 자신이 Desirable 모드 이면 상대방이 Access 모드 일때만 자신도 Access 모드가 되는 것이다.

 

# 자신이 Desirable 모드 이면 상대방이 Trunk, Desirable, Auto 모드 일때 자신은 Trunk 모드가 되는 것이다.

   Switch1(config)#  int fa1/0

   Switch1(config)#  switchport mode dynamic desirable

 

# Auto 모드 : Auto 모드는 Desirable 모드와 비슷하게 동작한다. 자신이 Auto 일때 상대방이 Auto, Access 이면 자신도 Access로 동작 자신이 Auto 일때 상대방이 Trunk, Desirable 이면 자신은 Trunk 로 동작한다.

   Swtich1(config)#  int fa1/0

   Switch1(config)#  switchport mode dynamic auto

 

# Nonegotiate : Nonegotiate는 Trunk 모드 일때 상대방에세 DTP 프레임을 보내지 말라는 옵션이다. 만약 두 스위치에 포트가 모드 Trunk 로 설정되어 있다면 DTP 프레임을 보낼 필요가 없다. 하지만 한쪽 혹은 양쪽 모두 Dynamic Desirable이나 Auto로 설정되면 DTP 프레임을 보내서 서로 마춰야 한다. 그래서 이 Nonegotiate는 Dynamic 모드에서는 사용이 불가능하다.

 

# Trunk 에서 VLAN 설정 : 일반적으로 Trunk는 모든 VLAN을 지나다닐수 있다. 하지만 여기에 모든 VLAN이 아닌 특정 VLAN만 지나다닐 수 있도록 할 수 있다.

   Switch1(config)#  int fa1/0

   Switch1(config)#  switchport trunk allowed vlan [번호, add 번호, except 번호, none, remove 번호]

  add : Trunk사용가능 VLAN 리스트에 특정 VLAN을 추가할때 사용하는 옵션

  except : 모든 VLAN을 사용 가능하게 하고 특정 VLAN 만 제외할때 사용하는 옵션

  none : 어떤 VLAN 도 사용하지 못하게 할때 사용하는 옵션

  remove : 사용가능한 VLAN 리스트중 특정 VLAN을 제거할때 사용하는 옵션

 

# VTP(Vlan Trunking Protocol) : VTP는 여러개의 스위치들이 서로 VLAN정보를 주고받을 때 사용하는 프로토콜이다. 한 스위치에서 VLAN설정을 하면 다른 스위치들이 VLAN정보를 받아가 설정는데 편리함은 있지만 VLAN정보가 다 지워지는 문제점도 있다.

 

# VTP는 Cisco에서 개발한 프로토콜로 Cisco스위치에서만 동작한다.

 

# VTP의 동작원리 : VTP 동작원리는 간단하다. 로컬 스위치에서 VLAN 정보를 변경하면 우선 순위 카운트(Configuration Revision)을 1씩 증가하여 다른 스위치에거 전송한다. 이때 Trunk 로 연결된 다른 스위치가 VTP 정보를 수신할때 우선순위가 자신보다 높으면 VLAN 정보를 일치시키지만 자신의 우선순위와 동일하면 무시하고, 자신의 우선순위보다 낮으면 자신의 VTP 정보를 로컬 스위치에게 전송한다.

 

VTP를 사용하기 위한 3가지 조건

 

# 스위치간에는 Trunk로 구성해야 한다.

# VTP 도메인이 동일해야 한다.

# VTP 정보를 교환할때 인증을 실시할 경우 패스워드가 동일해야 한다.

# VTP도메인 이름과 패스워드가 일치하면 다음과 같이 VTP 모드를 활용하여 VLAN 정보를 공유한다. 각각의 모드에는 서버, 클라이언트, 트랜스패런트가 있으며 스위치 VLAN 정보가 초기화되면 기본으로 서버 모드로 동작한다.

 

# VTP 서버 모드 : 모든 스위치가 처음에 기본적으로 사용하는 모드

                          VLAN 생성, 삭제, 수정이 가능, VLAN 데이터베이스 관리 권한이 있다.

                          VLAN 정보를 전파(광고)가 가능하다.

                          VLAN 정보를 일치(대체)가 가능하다.

                          VLAN 정보를 전달(중계)가 가능하다.

                          Standard VLAN(1~1005)만 사용 가능하다.

 

# VTP 클라이언트 모드 : VLAN 생성, 삭제, 수정이 불가능 하며 서버에게 요청을 실시하여 VLAN 정보를 공유한다.

                                   VLAN 정보를 전파(광고)가 불가능하다.

                                   VLAN 정보를 일치(대체)가 가능하다.

                                   VLAN 정보를 전달(중계)가 가능하다.

                                   우선순위가 자신보다 높으면 일치시키고 비슷하거나 낮으면 무시한다.

 

# VTP 트랜스패런트 모드 : VLAN 생성, 삭제, 수정이 가능, VLAN 데이터베이스 관리 권한이 있다.

                                      VLAN 정보를 전파(광고)가 불가능하다.

                                      VLAN 정보를 일치(대체)가 불가능하다.

                                      VLAN 정보를 전달(중계)가 가능하다.

                                      Standard VLAN(1~1005) 및 Extended VLAN(1006~4094) 까지 생성이 가능하다.

                                      VLAN Configuration Revision 카운트는 항상 '0' 이다.

 

# CISO IOS 기반에서는 Global Exec 모드에서 설정한다.

   Switch1(config)#  vtp domain CISCONAME    <= VTP 도메인을 'CISCONAME' 으로 설정

   Switch1(config)#  vtp password CISCO        <= VTP 패스워드를 'CISCO' 로 설정

   Switch1(config)#  vtp mode [ server | client | transparent ]

   Switch1(config)#  vtp pruning                     <= 트렁크로 불필요하게 실시되는 브로드케스트 차단 기능 실시

 

# CatOS 기반에서는 VLAN Database 설정 모드에서 설정한다. 'exit' 로 나가야 한다.

   Switch1#  vlan database

   Switch1(vlan)#  vtp domain CISCONAME

   Switch1(vlan)#  vtp password CISCO

   Switch1(vlan)#  vtp [ server | client | transparent ]

   Switch1(vlan)#  vtp pruning

   Switch1(vlan)#  exit

 

   Switch1#  show vtp status

    VTP Version                     : 2
    Configuration Revision          : 3
    Maximum VLANs supported locally : 256
    Number of existing VLANs        : 9
    VTP Operating Mode              : Server
    VTP Domain Name                 : CISCONAME
    VTP Pruning Mode                : Disabled
    VTP V2 Mode                     : Disabled
    VTP Traps Generation            : Disabled
    MD5 digest                      : 0x68 0x25 0xED 0x5F 0xA4 0x5C 0x8E 0x9B
    Configuration last modified by 13.13.11.1 at 3-1-02 00:33:28
    Local updater ID is 13.13.11.1 on interface Vl11 (lowest numbered VLAN interface found)

   Switch2#  show vtp status

    VTP Version                     : 2
    Configuration Revision          : 0
    Maximum VLANs supported locally : 256
    Number of existing VLANs        : 7
    VTP Operating Mode              : Transparent
    VTP Domain Name                 : CCNANAME
    VTP Pruning Mode                : Disabled
    VTP V2 Mode                     : Disabled
    VTP Traps Generation            : Disabled
    MD5 digest                      : 0x99 0xD8 0x2F 0x33 0x0A 0x8F 0x4D 0x33
    Configuration last modified by 13.13.11.2 at 3-1-02 00:31:54

 

# VTP 설정 초기화 : S1에는 VLAN11, VLAN12 가 설정되어 있고 VTP 설정 번호는 8이라고 한다.

 만약 VTP 설정 번호가 10이고 VLAN1만 설정되어 있는 S2를 S1과 연결할 경우 S1은 자신보다 VTP 설정 번호가 높은 S2의 VLAN 정보를 받고 자신의 VLAN번호 11, 12를 삭제해 버린다.

 결과적으로 S1은 자신의 VLAN이 없어지고 다운이 되버린다.

 이같은 사태를 방지하기위해 새로운 스위치를 추가할때는 VTP 설정 번호를 초기화 시켜야 한다.

 

# VTP 설정 번호를 초기화 시키기 위해서는 VTP 모드는 트랜스패런트로 설정했다 다시 서버나, 클라이언트로 변경하면 된다.

   Switch1(config)#  vtp mode transparent

   Switch1(config)#  vtp mode [ server | client ]


STP

# STP가 동작하면 물리적으로 루프 구조인 네트워크에서 특정 포트를 차단 상태로 바꾸어 루프가 발생하지 않게 한다. 그러다가 동작중인 스위치나 포트가 다운되면 차단 상태의 포트를 다시 전송 상태로 바꾸어 전체 네트워크가 다운되는 것을 막아준다.

 

# 루프르 방지하는 원리는 스위치 특정 포트를 논리적으로 동작 못하게 블락킹하는 것이다. 그러나 대규모 네트워크 환경에서는 사용자가 직접적으로 루프의 위치를 감지하고 블락킹 포트를 선정하는 것은 무리이며 네트워크 환경에 대한 변화를 알아내는 것은 쉽지 않다.

 

# 이런 문제를 해결하기 위해서 STP는 스위치들간에 BPDU프레임을 멀티케스트 방식으로 교환하여 스위치들간에 물리적인 연결상태와 네트워크에 대한 변경 사항 및 루프의 위치를 결정하며 최종적으로 블락킹 포트를 자동으로 선정한다.

 

# 스위치의 CPU 부하 확인하기

  S1#  show  process  cpu

 

# STP가 동작하지 않으면 CPU 사용율은 99%까지 올라가고 스위치가 다운된다. 이렇게 스위치에 루프가 발생하면 심각한 상황이 되므로 스위치 네트워크를 조정할 때는 항상 주의해야 한다. 이중화 네트워크에서는 STP가 제대로 동작하지 않으면 생길수 있는 또 다른 문제는 특정 호스트가 동일한 프레임을 중복해서 수신할 수도 있다.

 

# BPDU - 스위치들은 설정 BPDU를 교환하여 루트 브릿지를 선출하고 각각의 스위치 해당 포트를 지정포트(Designated Port)와 루트포트(Root Port)로 선정한다. 이때 설정 BPDU는 항상 루프 브릿지가 생성하여 전송한다. 만약 네트워크 환경이 변경되면 루트 브릿지가 아닌 다른 스위치들도 TCN BPDU를 생성하여 루프브릿지에게 토폴로지 변경 사항을 알린다. (2초 주기로 멀티케스팅)

 

# 루트 브릿지 선정과정

  1. Bridge ID의 우선순위가 가장 작은 스위치가 루트 브릿지로 선출되며 우선순위가 동일한 경우 MAC 주소가 가장 작은 스위치가 선출

  2. 루트 브릿지가 선출된 이후 나머지 스위치들은 RP를 선정한다. 여기서 RP란 BPDU를 수신하는 포트이며 루트 브릿지에게 가장 가까운 포트로 선정된다.

  3. 스위치와 스위치가 연결된 세그먼트 구간에는 DP와RP가 쌍으로 있어야 한다. 여기서 DP는 BPDU를 송싱하는 포트이며 보통 루트포트 반대편에 위치해 있다. 또한 루트 브릿지가 선출되면 루트 브릿지가 BPDU를 생성하여 전송하기 때문에 기본적으로 루트 브릿지의 포트들은 DP로 선정된다.

  4. 루트 브릿지가 선출되고 DP 및 RP가 선정되면 최종적으로 남은 포트를 대체 포트로 선정하여 블락킹 상태로 차단한다. 이러한 대체 포트를 갖고 있는 스위치를 Non 루트 브릿지라고 한다.

 

# STP 우선순위 변경 방법 

 SW1(config)#  spanning-tree  vlan  1  priority  4096

 

 # Blocking 과정

1. Root-Bridge 선출

 1) Bridge-ID 우선순위가 가장 낮은 스위치
 2) Bridge-ID MAC 주소가 가장 낮은 스위치

2. 포트 선정

 1) RP(Root Port) : BPDU 수신 포트
 2) DP(Designated Port) : BPDU 송신 포트

3.Blocking 포트 선정

 1) 루트 브릿지에서 Cost값이 높은 포트 차단
 2) Sender-Bridge-ID가 높은 구간 포트 차단
 3) Port-ID가 높은 구간 포트 차단

# BPDU(Bridge Protocol Data Unit)

 - STP가 동작하는 스위치간에 2초 주기로
   멀티케스트를 하여 교환하는 메세지

 

 - BPDU (Configuration BPUD)

 1. Bridge-ID
 2. Cost
    (10M : 100, 100M : 19, 1G = 4, 10G = 2)
 3. Port-ID : 128.포트번호
 ----------------------/ 비교&조건 단위

 4. Hello-Time : 2초
 5. Forward-Delay-Time : 15초
 6. Max-Age-Time : 20초

 

IEEE 8021.d STP 포트 상태

# Blocking(BLK) - 포트가 논리적으로 차단된 상태이며 이 상태에서는 프레임 송수신이 불가능하며 오로지 BPDU 수신만 가능하다. 또한 블락킹 상태는 상대방 스위치로부터 MAX 에이지(20초) 동안 BPDU 를 수신하지 못하면 발생된다.

 - 포트 차단 상태
 - BPDU만 수신 가능(BPDU 송신 X, 프레임 송수신 X)

 

# Listening(LIS) - 블락킹 상태의 다음 단계이며 해당포트를 Forwarding 상태로 변경하기 위해서 루프 검사를 하는 상태이다. 이상태에서는 BPDU 전송이 가능하며 블락킹 상태 다음 단계로 진생될 수 있거나 또는 포트에 PC, 라우터, 스위치가 연결되면 Listening 단계부터 시작된다. 기본 15초 동안 실시

 - 루프 체크 단계
 - BPDU만 수신 가능(BPDU 송신 X, 프레임 송수신 X)
 - Blocking 포트가 열릴때 & 사용하지 않은 포트에 노드를 연결할때 시작
 - 15초 (Forward-Delay-Time)

 

# Learning(LRN) - Listening 상태의 다음 단계이며 해당 포트를 Forwarding 상태로 변경하기 위해서 MAC 주소를 학습하여 MAC 주소 테이블을 생성하는 상태이다.

 - MAC 주소 학습 단계
 - BPDU 송수신 가능(프레임 송수신 X)
 - Listening 다음에 시작
 - 15초 (Forward-Delay-Time)

 

# Forwarding(FWD) - Learning 상태의 다음 단계이며 해당 포트를 이용하여 프레임 송수신이 가능한 상태이다.

 - 프레임 송수신 가능 상태
 - BPDU 송수신 가능, 프레임 송수신 가능


STP의 동작원리

# STP의 동작원리

STP의 동작 원리를 이해하기 위해서는 3가지를 아셔야 합니다.  3가지는 바로 루트(Root) 스위치, 루트 포트, designnated 포트 입니다.

루트 스위치는 하나의 STP가 동작하는 스위치 네트워크 상에서는 하나의 루트 스위치가 선출된다.

이렇게 루트 스위치가 선출되고 나면, 다른 스위치들은 각각 자신의 포트 중에서 루트 포트를 하나씩 선출하게 됩니다.

루트 스위치와 루트 포트를 선출하고 나서, 마지막으로 스위치들의 나머지 포트들 중에서 한 세그먼트(segment)당 하나의 designtaed 포트가 선출이 됩니다.

세그먼트는 스위치와 스위치 사이를 연결하는 라인이라고 보시면 됩니다.

여기까지 하고도 남은 포트가 있으면 그 나머지 포트들은 자동적으로 대체 포트(alternate port)가 되는데, 이 포트들은 자동으로 차단됩니다.

이렇게 위와 같은 과정을 거치게되면, 자동적으로 루프 문제가 해결되며 하나의 스위치 네트워크상에서 모든 스위치끼리 통신이 원활히 이루어지게 됩니다. 이제부터 하나씩 선출과정을 알아보겠습니다.

먼저 아래와 같이 하나의 스위치 네트워크를 구성해 보았습니다.

<그림 10-1>

 

1) 루트 스위치 선출하기

먼저 루트 스위치에 선출과정에 대해서 알아보겠습니다.

우선 하나의 스위치 네트워크 상에서 모든 스위치가 루트 스위치가 될 수 있는 자격이 있습니다. 그럼 누가 루트 스위치가 될까요? 

바로 브리지ID가 낮은 스위치가 루트 스위치로 선출이 됩니다.

브리지ID에서 priority 값을 비교하는데 기본으로 32768입니다. 이 값이 똑같으면 그 다음 비교하는게 MAC주소 입니다.

MAC주소 역시 가장 낮은 주소를 가진 스위치가 바로 루트 스위치가 되는 것입니다.

그럼 위 그림10-1에서 MAC주소를 비교해 보면 어떤 스위치가 루트 스위치가 될까요?

<그림 10-2>

바로 MAC주소가 가장 낮은 스위치1이 루트 스위치로 선출되었습니다. 이 루트 스위치에서 BPDU 프레임을 모든 스위치로 뿌립니다. 

이제 다음 단계인 루트 포트 선출하는 과정을 알아볼까요??^^

 

2) 루트 포트 선출하기

루트 포트는 루트 스위치가 아닌 다른 모든 스위치에서 의무적으로 하나씩 선출해야 합니다.

선출하는 기준은 ① 루트 스위치 ID  ② 경로값(path cost) ③ 브리지 ID ④ 포트 ID 이렇게 4가지 순서를 차례로 비교해 가장 낮은 값을 가진 스위치의 포트가 루트 포트로 선출되게 됩니다.

그럼 다시 그림10-2에서 보면, 루트 스위치인 스위치1을 제외한 스위치2,3에서 하나씩 루트 포트를 선출하겠죠?

그럼 우선 스위치2를 먼저 볼까요?

루트 포트를 선출하는 순서 중에서 경로값을 제외한, 루트 스위치 ID, 브리지 ID, 포트 ID는 어제 배운 BPDU에 들어있는 정보입니다.

그러므로 이 스위치 네트워크 상에서는 루트 스위치인 스위치1만 BPDU를 뿌려주므로 스위치2의 fa0/23 포트나 fa0/21 포트가 똑같은 값을 가지게 됩니다. 결국 여기선 경로값을 가지고 루트 포트를 선출하게 되겠네요.

이 스위치 네트워크는 모두 패스트 이더넷 구간이니 경로값이 19가 되겠죠?

그럼 루트 스위치까지 가는 경로값의 합을 구해야 되는데 어느 경로가 더 값이 작을까요?

<그림 10-3>

fa0/23 포트로 가면 19이고, fa0/21 포트로 가면 19 + 19 = 38 이 되죠~ 그러므로 fa0/23 포트가 루트 포트로 선출되었습니다.

그럼 스위치3도 똑같겠죠? 스위치3은 fa0/19 포트가 루트 포트가 된다는 걸 아실겁니다.

 

3) designated 포트 선출하기

designated 포트는 하나의 세그먼트 당 하나씩 뽑는다.. 세그먼트는 스위치끼리 연결된 라인이다..

그럼 그림10-3에서 보면, 스위치1-2 사이, 스위치1-3사이, 스위치2-3사이 이렇게 3구간에서 각각 designated 포트를 선출해야겠네요.

스위치1-2사이에선 스위치2쪽이 루트 포트로 선출되었으니 나머지 스위치1쪽의 포트가 자동으로 designated 포트로 선출 될 것입니다.

마찬가지로 스위치1-3사에서도 스위치3쪽 포트가 루트 포트 이므로, 스위치1쪽의 포트가 자동으로 designated 포트로 선출되겠죠~

그럼 마지막 스위치2-3구간만 남았네요.. 여기선 양쪽 모두 아무 포트도 선출되지 않았으므로 양쪽 다 designated 포트가 되기 위한 후보가 될 수 있겠네요~

designated 포트도 루트 포트와 똑같은 기준으로 선출과정을 거칩니다.

즉, ① 루트 스위치 ID  ② 경로값(path cost) ③ 브리지 ID ④ 포트 ID 이 4가지 과정을 순서대로 비교해서 선출하는 것입니다.

그럼 스위치2-3을 비교해 볼까요?

우선 루트 스위치 ID는 동일하고, 경로값 또한 38로 똑같습니다. 세번째인 브리지ID도 priority 값은 32768로 같은데, 아! MAC주소가 다르군요~

그럼 어느 MAC주소가 더 작은지.. 비교해보면 designated 포트가 선출되겠네요?^^

<그림 10-4>

바로 스위치2의 fa0/21 포트가 designated 포트로 선출되었습니다.

마지막으로 남은 스위치3의 fa0/21 포트는 자동적으로 대체 포트로 선출이 되겠죠?^^

그리고 대체 포트는 자동적으로 차단이 되는 것까지 알 수 있겠습니다.

아래 그림10-5가 바로 대체 포트까지 선출된 완전한 STP가 동작하는 모습입니다.

<그림 10-5>

BPDU는 루트 스위치로부터 그림과 같이 화살표 방향으로 전파 됩니다.

이처럼 완전한 STP가 동작하게 되면 루프가 없는 스위치 네트워크가 구성이 되며, 만약 스위치3의 fa0/19 포트가 차단된다면 대체 포트인 fa0/21 포트가 차단이 해지되어 스위치 네트워크 상의 중단없는 원활한 흐름을 계속 가져갈 수 있게 해줄 것입니다.


STP 포트의 상태변화

# STP 포트의 상태 변화


루트 스위치와 루트 포트, designated 포트를 모두 선출 하였습니다. 그래서 루트 스위치가 다른 모든 스위치들에게 BPDU를 전파 한다고도 했었구요.. 그럼 여기서, 스위치의 포트가 로트 포트 또는 designated 포트가 되었다면 바로 루트 스위치로 부터 BPDU를 받거나, 아니면 데이타 프레임 같은 정보를 받을 수 있을까요?

결론부터 말씀 드리자면,

스위치는 5단계를 거쳐 데이타를 주고 받을 수 있는 Forwarding 상태가 됩니다.

이 5가지 단계는 Disabled -> Blocking -> Listening -> Learning -> Forwarding 과 같은 상태 변화를 거쳐 최종적으로 Forwarding 상태에 이르게 됩니다.

데이타 프레임과는 다르게, BPDU 정보는 처음 상태인 disabled 상태만 아니면 나머지 4단계 상태에서는 루트 스위치로 부터 받을 수 있습니다.

그럼 이 5가지 상태의 변화를 천천히 하나씩 알아보겠습니다.

 

1. Disabled 상태

  포트가 고장났거나 shut down된 경우. 이 상태에서는 데이타와 BPDU 모두 주고 받을 수 없으며, MAC주소 또한 배우지 못합니다.

 

2. Blocking 상태

  shut down 된 포트를 살리거나 스위치를 재부팅 시킨 경우에 해당됩니다. 이 상태에서는 데이타는 주고받지 못하고, BPDU를 받을 수만 있습니다. 하지만 MAC주소는 여전히 배울 수가 없습니다. 어제 배운 루트 스위치를 선출하고 루트 포트, designated 포트를 선출하는 과정이 바로 이 Blocking 상태에서 일어나는 것입니다.

 

3. Listening 상태

  2단계인 Blocking 상태에서 루트 포트나 designated 포트를 선출하면 스위치 포트는 바로 Listening 상태로 넘어가게 됩니다. 

  Blocking 상태에서 Listening 상태로 넘어오기 위해서는 에이징 타임은 20초를 기다려야 비로소 바뀌게 됩니다.

  이때에도 아직은 데이타의 전송은 불가능하며, BPDU만 주고 받을 수 있습니다.  이 Listening 상태가 되야 루트 스위치가 드디어 BPDU를 보내기 시작하는 것입니다. 하지만 MAC주소는 아직은 배우지 못하고 있습니다.



4. Learning 상태

  3단계 Listening 상태의 스위치 포트가 Forwarding delay 시간, 즉 포워딩 지연 시간(default:15초) 이 유지된다면, Learning 상태로 넘어오게 됩니다. 이 Learning 상태가 되면 비로소 MAC주소를 배우고 MAC주소 테이블을 만들기 시작합니다.

  그리고 BPDU도 주고 받지만, 아직도 데이타는 주고 받지 못합니다.

 

5. Forwarding 상태

  Listening 상태에서 15초(default forwarding 시간)를 기다리고 다시 forwarding delay 시간인 15초동안 스위치의 포트가 이 상태를 유지한다면, Forwarding 상태로 바뀌게됩니다. 이러면 BPDU 뿐만 아니라, 드디어 데이타 프레임도 주고 받을 수 있게됩니다.

  이러한 5단계의 상태 변화를 거친 포트가 Fowarding 또는 Blocking 상태가 되면서, 전체 스위치 네트워크는 자연히 최적 경로만 남게 되고 루프 방지를 위한 STP가 동작하게 되는 것입니다.

그럼 실제로 변화는 과정을 한번 확인해 볼까요?

<그림 11-1>

스위치2가 루트 스위치인 스위치 네트워크 구성입니다.

스위치3을 보면 fa0/19 포트가 차단된 상태이며, fa0/21 포트로 루트 스위치로 부터 정보를 주고 받겠죠.. 그럼 간단히 먼저 확인해 볼까요?

Switch3#sh spanning-tree

VLAN0001
  Spanning tree enabled protocol ieee
  Root ID    Priority    32769
             Address     000d.ed25.9900
             Cost        19
             Port        23 (FastEthernet0/21)
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec

  Bridge ID  Priority    32769  (priority 32768 sys-id-ext 1)
             Address     0018.ba36.0280
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec
             Aging Time 300

Interface        Role Sts Cost      Prio.Nbr Type
---------------- ---- --- --------- -------- --------------------------------
Fa0/19           Altn BLK 19        128.21   P2p
Fa0/21           Root FWD 19        128.23   P2p

그럼 이제 포트의 상태 변화를 보기위해, 기존 스위치3의 fa0/21번 포트를 차단해 보겠습니다.

그렇게 되면 기존에 차단되있던 fa0/19번 포트가 살아나겠죠?

이때, Blocking에서 부터 Forwarding까지의 변화를 살펴 보겠습니다.

Switch3(config)#int fa0/21
Switch3(config-if)#shut

Switch3#sh spanning-tree

                         :
                     (생략)

Interface        Role Sts Cost      Prio.Nbr Type
---------------- ---- --- --------- -------- --------------------------------
Fa0/19           Root LIS 19        128.21   P2p

포트가 Blocking 상태에서 Listening 상태로 넘어갔습니다.

Switch3#sh spanning-tree

                         :
                     (생략)

Interface        Role Sts Cost      Prio.Nbr Type
---------------- ---- --- --------- -------- --------------------------------
Fa0/19           Root LRN 19        128.21   P2p

그 다음 Learning 상태로 바뀐 모습이구요..

Switch3#sh spanning-tree

                         :
                     (생략)

Interface        Role Sts Cost      Prio.Nbr Type
---------------- ---- --- --------- -------- --------------------------------
Fa0/19           Root FWD 19        128.21   P2p

자, 드디어 Forwarding 상태로 바뀌었습니다.^^

그러나 Blocking에서부터 Forwarding까지 변하는 시간이 20초+15초+15초 = 50초,

즉, 다운 상태의 포트가 Forwarding 되기 위해서는 50초라는 시간이 필요한 것입니다.

생각보다 오래 걸리죠?^^

이 긴 컨버젼스 타임이 STP의 단점이 되기도 하는데요, 이러한 단점을 보완하기 위해 나온것이 RSTP(Rapid STP)입니다.

RSTP는 컨버젼스 타임을 획기적으로 단축시켜 Forwarding 상태까지 급격히 변하게 됩니다.


STP 컨버전스 타임 조정

# STP 컨버전스 타임 조정하기

전에 말씀드릴땐 Blocking에서 Forwarding되는 시간이 20+15+15초 해서 총 50초가 걸린다고만 말씀 드리고 넘어갔습니다.

이번 시간에 자세히 말씀드리면, Blocking에서 Listening까지 걸리는 시간이 20초이고, 이를 맥스 에이지(max age)라고 합니다.

그리고 Listening에서 Learning까지 걸리는 시간이 15초, Learning에서 Forwarding까지 걸리는 시간이 15초입니다.

그래서 총 50초가 소요되어, 컨버젼스 타임이 50초가 되는 것입니다.

 

그럼 이제 본론으로 들어가서.. 컨버젼스 타임은.. 어떠한 추가나 수정, 삭제 등으로 인한 설정으로 토폴로지가 변화했을 때 이것반영해서 네트워크가 재구성될 때까지 소요되는 시간을 말합니다. 저번에 언급했듯이 STP가 가지는 큰 문제점 중에 하나가 이 컨버젼스 시간이 너무 길다는 것 입니다.

그럼 어떤 문제가 생길까요?

STP를 사용하는 이유가 루프를 방지하기 위해서였죠.. 그리고 다운이 될 경우 막아놨던 링크를 Forwading 시켜 네트워크가 중단없이 원활한 통신이 되야하는거죠~

그런데, 어제 배운 STP가 동작하는 스위치 포트상태의 변화를 보면 Blocking에서 Forwarding까지가는 시간, 즉 컨버젼스 타임이 50초나 걸렸죠. 그럼 결국 이 50초 동안은 네트워크가 멈춰버리게 되는 것입니다. 즉, STP가 제 역할을 하지 못한다고 볼 수가 있습니다. 이제 이 컨버젼스 타임을 줄여봐야겠죠?^^

먼저 시스코에서 나온 포트 패스트(portfast), 업링크 패스트(uplinkfast), 백본 패스트(backbonefast) 이 3가지에 대해 알아보겠습니다.


1. 포트 패스트(portfast)

  포트 패스트는 PC나 서버 등 종단 시스템(end system), 즉 가장자리에 있는 스위칭이 필요없는 장비와 연결된 포트에서 사용을 합니다. 스위치가 부팅되는 순간 바로 Forwarding 상태로 변하게 됩니다. 50초간의 컨버젼스 타임없이 순식간에 Forwarding 된다니.. 신기하네요^^

그럼 같이 설정해 볼까요?

 

[포트 패스트 활성화 시키기]
Switch1(config)#  spanning-tree portfast default
%Warning: this command enables portfast by default on all interfaces. You
 should now disable portfast explicitly on switched ports leading to hubs,
 switches and bridges as they may create temporary bridging loops.

이처럼 디폴트 명령어로 전체 포트에서 포트 패스트를 활성화 시킬 수 있습니다.

하지만 이렇게 하면 종단 장비가 아닌 모든 장비와 연결된 포트가 전부 포트 패스트 활성화가 되서 문제가 생기겠죠?

경고 문구처럼 이 명령어는 주의해서 설정하셔야 합니다.

그러므로 다음과 같이 포트별로 포트 패스트 활성화 시키는 것을 권장합니다.

 

[포트별 포트 패스트 활성화 시키기]
Switch1(config-if)#  spanning-tree portfast
%Warning: portfast should only be enabled on ports connected to a single
 host. Connecting hubs, concentrators, switches, bridges, etc... to this
 interface  when portfast is enabled, can cause temporary bridging loops.
 Use with CAUTION

%Portfast has been configured on FastEthernet0/1 but will only
 have effect when the interface is in a non-trunking mode.

이번에도 경고 문구가 나오죠.. 포트 패스트가 활성화된 인터페이스에 허브, 스위치 등이 연결하면 일시적으로 루프가 발생할 수도 있다고.. 머 그런소리네요^^

실제적으로 BPDU가 차단되고 일시적인 루프 현상이 발생할 수도 있다는 단점이 있습니다.

그럼 간단히 확인 후 다음 업링크 패스트를 알아보겠습니다.

 

[포트 패스트 설정 확인하기]
Switch1#  sh spanning-tree summary
Switch is in pvst mode
Root bridge for: VLAN0001
Extended system ID                is enabled
Portfast Default                      is enabled
PortFast BPDU Guard Default  is disabled
Portfast BPDU Filter Default     is disabled
Loopguard Default                  is disabled
EtherChannel misconfig guard is enabled
UplinkFast                              is disabled
BackboneFast                        is disabled
                 :

2. 업링크 패스트(uplinkfast)

  업링크 패스트는 자신에게 직접 연결된 링크가 다운되었을 때, 차단 상태였던 포트를 즉시 Forwarding 시키는 역할을 합니다.

  링크가 다운되면, 차단 되었던 포트는 Blocking에서 Listening으로 상태가 변하게 됩니다.

  그럼 Listening에서 Forwarding까지의 시간, 15 + 15 = 30초의 시간이 소요됩니다. 이때 업링크 패스트를 설정하면 이 30초의 시간을 단축하여 바로 Forwarding으로 만들 수 있게 됩니다.

<그림 13-1>

위 그림처럼 스위치3에 업링크 패스트를 설정한 후 fa0/22 포트가 다운되면, 차단되어있던 fa0/20 포트가 Forwarding 상태로 바로 바뀌게 됩니다.

같이 한번 설정해 보겠습니다.~

 

[업링크 패스트 설정하기]
Switch3(config)#  spanning-tree uplinkfast

그럼 이제 한번 스위치3의 fa0/22 포트를 다운시키고 fa0/20 포트가 어떻게 변하는지 살펴볼까요?

 

[업링크 패스트 디버깅하기]
Switch3#  debug spanning-tree uplinkfast

Switch3(config)#  int fa0/22
Switch3(config-if)#  shutdown

00:22:44: STP FAST: UPLINKFAST: make_forwarding on VLAN0001 FastEthernet0/20 root port id

new: 128.24 prev: 128.23

00:22:44: %SPANTREE_FAST-7-PORT_FWD_UPLINK: VLAN0001 FastEthernet0/20 moved to Forwarding (UplinkFast).
 

3. 백본 패스트(backbonefast)

  자 마지막 백본 패스트는 업링크 패스트와 구분하여, 직접 연결된 링크가 아닌 간접 링크가 다운되었을 경우에 해당이 됩니다.

  이 때, 차단 상태의 포트를 Blocking에서 맥스 에이지(20초) 시간을 단축하고 바로 Listening 상태바꿔주는 역할을 합니다.

  즉, 컨버젼스 타임이 20초가 줄어들어 30초가 되는 것이겠죠^^

그럼 같이 설정해 볼까요?

<그림 13-2>

 

[백본 패스트 설정하기]
Switch3(config)#spanning-tree backbonefast

이처럼 설정한 후 스위치3의 fa0/22 포트를 다운시킨후 스위치1에서 확인해 보겠습니다.

 

[백본 패스트 디버깅하기]
Switch3(config)#  int fa0/22
Switch3(config-if)#  shutdown

Switch1#  debug spanning-tree backbonefast

00:44:49: STP: VLAN0001 heard root 28673-000d.6576.e980 on Fa0/19
00:44:49: STP: VLAN0001 Fa0/19 -> listening
00:44:50: STP: VLAN0001 Topology Change rcvd on Fa0/19
00:44:50: STP: VLAN0001 sent Topology Change Notice on Fa0/24
00:44:56: STP: VLAN0001 Fa0/19 -> learning
00:45:12: STP: VLAN0001 sent Topology change Notice on Fa0/24
00:45:12: STP: VLAN0001 Fa9/19 -> forwarding

자, 컨버젼스 타임을 단축시키기 위해 이처럼 3가지에 대해서 모두 알아보았습니다. 그럼 마지막으로 하나만 더 알아볼까요?

컨버젼스 타임을 줄이기 위한 마지막 방법이 있습니다.

바로, 직접 STP의 헬로타임, 맥스에이지, Forward delay를 조정하여 줄이는 것입니다. STP에서 기본적으로 50초가 컨버젼스 타임인데, 우선 먼저 확인해 볼까요?

 

[스패닝 트리 타이머 확인하기]
Switch2#  sh spanning-tree

VLAN0001
  Spanning tree enabled protocol ieee
  Root ID    Priority    32769
             Address     000d.ed25.9900
             This bridge is the root
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec

  Bridge ID  Priority    32769  (priority 32768 sys-id-ext 1)
             Address     000d.ed25.9900
             Hello Time   2 sec  Max Age 20 sec  Forward Delay 15 sec
             Aging Time 300

이제 이 타이머들의 시간을 줄여보도록 하겠습니다. 그런데 여기서 주의할 점이 한가지 있습니다.

이 헬로타임이나 맥스에이지, forward delay를 마음대로 줄여 컨버젼스 타임을 단축시키면 좋겠지만, 그렇게되면 루프가 발생할 확률이 커지게 됩니다. 그래서 디아미터(diameter), 즉 미리 정해놓은 안전한 값들을 확인하여 조정해줘야 합니다.

그럼 2-7까지 디아미터별로 적정 타이머 값을 확인해 보겠습니다.

*diameter         2     3     4    5    6     7

hello time         2     2     2    2    2     2

max age          7     9   10   12   13   15

forward delay  10   12   14   16   18   20

보시는 바와 같이 기본값은 디아미터7을 따르고 있네요. 그럼 디아미터2에 속하는 타이머값을 가지고 설정을 해보겠습니다.

 

[타이머 조정하기]
Switch2(config)#  spanning-tree vlan 1 hello-time 2
Switch2(config)#  spanning-tree vlan 1 forward-time 7
Switch2(config)#  spanning-tree vlan 1 max-age 10   

Switch2#  sh spanning-tree

VLAN0001
  Spanning tree enabled protocol ieee
  Root ID    Priority    32769
             Address     000d.ed25.9900
             This bridge is the root
             Hello Time   2 sec  Max Age 10 sec  Forward Delay  7 sec

  Bridge ID  Priority    32769  (priority 32768 sys-id-ext 1)
             Address     000d.ed25.9900
             Hello Time   2 sec  Max Age 10 sec  Forward Delay  7 sec
             Aging Time 300

타이머를 조정한 후 확인 결과, 컨버젼스 타임이 10 + 7 + 7 = 24로 확 줄어들었습니다.


 

PDF 자료  streams-zerocopy.pdf

 

Zero Copy는 머 간단히 이야기 하면 Network에서 Read/Write 할때 걸리는 불필요한 Copy 과정을 최소화 하자는 이야기이다

IBM(http://www.ibm.com/developerworks/library/j-zerocopy/) - 번역(http://highway101.tistory.com/301)

의 내용을 살펴 보면 기존의 데이터 복사 과정은 다음과 같다
 



 



  무려 4번이나 걸린다 - _- 굳이 그럴필요가 있는가 걍 바로 네트워크로 Read/Write하면 되지 않는가 !!

  그래서 얘네 정의 한 내용을 Zero Copy의 과정을 보면 다음과 같다

 



 보면 2번만에 복사가 완료된다 -

 근데 구현은 어떻게 할까 ㅡ.ㅡ?

 친절하게 Sample Code가 있다. 근데 java ... 어쩌라고 - _-

 다른 참고 사이트를 살펴 보면 (http://www.linuxjournal.com/article/6345) Zero Copy 에 관련하여 좀 더 자세히 나와 있다 = _=

 현재 소스포지(http://ko.sourceforge.jp/projects/sfnet_zero-copy/)에 linux 2.6.15.3 버전의 패치를 참고하여 작업진행 중.

 기타 code 다운 주소 : http://www.ioremap.net/archive/zero-copy/


한국어 판 Unix Network Programming, Stevens 저, 김치하, 이재용 역, 대영사, 1991 요약본입니다


 


출처 : http://fattarzan.tistory.com/entry/%EB%AC%B8%EC%84%9C-UNIX-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D

윈속 프로그래밍 리뷰 - 1

앞으로 몇 회에 걸쳐 윈도우 기반의 서버 프로그래밍 기법에 대해 살펴볼 것이다. 개념적인 설명보다는 실무에 바로 적용될 수 있는 내용들 위주로 설명할 것이다. 이후 대량의 접속을 커버할 수 있는 실전 예제를 만들어 보는 것으로, 독자의 수준을 크게 한 단계 업그레이드시키는 것이 이 글의 궁극적인 목표이다. 독자가 아직 윈속 API나 멀티 쓰레드에 대해 익숙하지 않다면 참고 서적을 옆에 두고 따라오는 것이 좋을 것이다. 
서버 프로그래밍을 하다 보면 정말 웃지 못할 사건들을 많이 접하게 된다. 클라이언트 접속이 1000개까지는 문제가 없다가 1001번째에 갑자기 크래시(crash)가 발생하기도 하고, 서버를 구동시킨 지 2~3일이 지난 시점부터 조금씩 리소스가 새기도 한다. 부지런한 개발자들은 MSDN의 버그 리포트와 테크니컬 기사를 샅샅이 뒤져보며 운영체제 또는 하드웨어의 버그이길 간절히 바란다. 

그러나 아쉽게도 이런 경우의 대부분이 본인의 실수에서 비롯되므로 유용한 단서를 찾기는 힘들다. 몇몇 뛰어난 프로그래머들은 몇 날 몇 일을 밤새워 고생하다 갑자기 무언가 드디어 알아챘다는 듯 멀쩡히 죄 없는 PC를 포맷하기도 한다(필자도 과거에 일말의 희망을 걸고 수차례 포맷해 본 경험이 있다. 물론 다시 설치한다고 해결될 성질의 것이 결코 아니었다). 앞으로의 설명은 서버의 성능이나 최적화 문제보다는 안정적이고 유연한 네트워킹 환경을 구축하는 것에 초점을 맞춰 진행할 것이다.

소켓 API 리뷰
초보 네트워크 개발자들이 겪는 문제들의 많은 부분이 소켓과 TCP의 특성을 제대로 이해하지 못한 것에서 시작한다. 다소 지루할 수도 있겠지만, 먼저 각 소켓 API의 중요한 점을 되짚어 보는 것으로 시작하겠다.

<리스트 1> 소켓 생성 함수
SOCKET socket(
int af, 
int type, 
int protocol 
);
SOCKET WSASocket(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,
DWORD dwFlags
);

소켓을 생성하는 함수는 두 가지가 있다. <리스트 1>처럼 WSASocket 쪽이 좀더 다양한 프로토콜을 열거할 수 있다. 하지만 우리가 만들 서버는 인터넷 TCP 프로토콜만을 사용하기 때문에 어느 것을 사용해도 문제없다. 단 WSASocket으로 소켓을 생성하는 경우 dwFlags 파라미터에 WSA_FLAG_OVERLAPPED를 넘겨 오버랩드 속성을 가지도록 해야 한다. 그렇지 않으면 이후 다룰 WSASend, WSARecv 등의 오버랩드 호출은 무시될 것이다. 오버랩드 I/O에 관한 내용은 추후 자세히 설명하겠다.

int bind(
SOCKET s, 
const struct sockaddr FAR *name, 
int namelen 
);

빈 소켓만으로는 아무것도 할 수 없다. 소켓과 로컬 주소를 연결시킨 뒤에야 비로소 네트워크 통신을 할 수 있는데, bind가 이러한 역할을 해 준다. 먼저 name 파라미터를 자세히 살펴보자(<리스트 2>).

<리스트 2> name 파라미터 
struct sockaddr {
unsigned short sa_family
char sa_data[14]};
struct sockaddr_in{
short sin_family
unsigned short sin_port
IN_ADDR sin_addr
char sin_zero[8]};


sockaddr은 bind할 주소를 지정하는데 쓰이는 16바이트 크기의 구조체다. 소켓에는 다양한 주소 패밀리(AF_UNIX, AF_INET, AF_IMPLINK, ...)와 각각의 하위 프로토콜이 존재한다. 각 주소 패밀리에 따라 주소 지정 방법이 다를 수 있는데, 우리는 인터넷 프로토콜(AF_INET)을 사용하므로 AF_INET의 주소 지정을 쉽게 하기 위해 우측의 sockaddr_in을 사용한다.

sockaddr_in의 sin_addr에 보통 ADDR_ANY를 집어넣어 멀티홈드 호스트(예 : 여러 LAN 카드가 꽂혀 있는 호스트)의 특정 네트워크를 신경쓰지 않고 프로그래밍한다. 그러나 성능이나 보안 측면을 강화시키기 위해 특정 네트워크의 주소를 입력할 수 있다.






윈속이 제공하는 Name Resolution 함수 중 하나인 gethostbyname를 사용해 로컬 호스트의 네트워크를 열거할 수 있다.

HOSTENT *he = gethostbyname( host_name );
he->h_addr_list[0]; // 첫 번째(예 : LAN CARD #1)
he->h_addr_list[1]; // 두 번째(예 : LAN CARD #2)
he->h_addr_list[2]; // 세 번째(예 : CABLE MODEM)

다음으로 sockaddr_in의 port를 살펴보자. 이 값이 0이면 시스템은 적당한 포트를 찾아 맵핑해 준다. 윈도우 2000에서의 기본 값은 1024~5000 사이의 값인데, 부족할 경우 TCP/IP 관련 레지스트리 키(MaxUserPort)의 최대 값을 변경할 수 있다.

MaxUserPort
Key: HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesTcpipParameters
Value Type: REG_DWORDmaximum port number
Valid Range: 500065534 (decimal)
Default: 0x1388 (5000 decimal)

이 키 외에도 MaxFreeTcbs와 MaxHashTableSize 등을 조절해 맵핑될 소켓 수를 조절할 수 있다. 관심있는 독자는 「Microsoft Windows 2000 TCP/IP Implementation Details」를 참고하기 바란다.

그리고 당연하겠지만 같은 주소로의 두 번째 bind는 실패한다. 간혹, 이전 bind된 소켓을 분명히 닫았음에도 두 번째 bind가 실패하는 경우가 있는데, 이것은 이전 소켓이 실제로 완전히 닫히지 않고 TIME_WAIT 상태에 머물러 있기 때문이다. 서버를 재시작하는 경우에도 발생할 수 있는데, 이런 상황을 피하려면 setsockopt 함수를 사용해 SO_REUSEADDR 옵션을 셋팅하면 된다. TCP 상태에 대해 잘 모르고 있다면 TCP/IP 서적 등을 참고해 반드시 숙지하기 바란다.

int connect(
SOCKET s, 
const struct sockaddr FAR *name, 
int namelen 
);

상대방 호스트에 접속하기 위해 connect를 호출한다. connect를 호출하기 위해 bind할 필요는 없다. 소켓을 생성한 뒤 바로 connect를 호출하면 자동으로 시스템이 지정하는 포트로 bind되는데, 이 역시 1024~5000 사이의 값을 가진다. 스트레스 테스트(stress test)를 하면 이 정도의 연결이 부족할 수 있다. 따라서 필요하다면 앞서 언급한대로 레지스트리 값을 적당한 값으로 설정해 주자.









윈속 프로그래밍 리뷰 -2

박범진  (oranze@wemade.com)
int listen(
SOCKET s,
int backlog
);

listen은 소켓에 TCP 접속을 받아들일 수 있는 속성을 부여해 준다. backlog 파라미터는 동시에 접속이 몰렸을 때를 처리하기 위한 큐의 크기인데, 보통 시스템의 최대 값을 지정해서 쓴다. 윈속 2.0 이전 버전에서 이 값의 최대 값은 5였는데, 이것은 접속 요청이 최대 5개까지 큐될 수 있다는 것을 뜻한다. 윈속 2가 등장하면서 SOMAXCONN이라는 상수 값을 사용하는데, 내부적으로 윈도우 2000 서버는 200개, 프로는 5개까지 설정된다. 접속 처리를 위해 accept를 호출하면, backlog 큐의 첫 번째 노드가 삭제되면서 다른 접속 요청을 큐에 넣을 수 있다. backlog 큐가 가득 차면 클라이언트의 connect 호출은 WSAECONNREFUSED 에러를 리턴한다.

SOCKET accept(
SOCKET s,
struct sockaddr FAR *addr,
int FAR *addrlen
);

accept는 서버 소켓의 접속 큐에서 첫 번째 노드를 가져와 소켓을 생성한 뒤 리턴한다. 리턴된 소켓은 s 파라미터와 동일한 속성을 가진다는 것을 기억해 두자.

<리스트 3> 리턴 함수의 사용
int send(
SOCKET s,
const char FAR *buf,
int len,
int flags
);
int recv(
SOCKET s,
char FAR *buf,
int len,
int flags
);

TCP 패킷을 주고받을 때 사용한다. 가장 빈번하게 호출되는 함수인 만큼 초보 네트워크 개발자들이 주의해야 하는 부분이다. TCP는 신뢰할 수 있는(reliable) 스트림 기반의 프로토콜이다. 여기서 스트림 기반이라는 것에 주목할 필요가 있다. 수신자는 언제나 송신자가 전송한 만큼 받기 마련이지만, 이것이 곧 send, recv 함수의 호출 횟수까지 같다는 것을 뜻하지는 않는다.

전송된 패킷은 인터넷의 수많은 게이트웨이를 경유하면서 상대방에게 도착하는데, send 호출과는 관계없이 패킷이 뭉쳐오기도 하고 완전히 조각난 상태로 도착하기도 한다. TCP는 보낸 순서대로 끝까지 도착하는 것을 보장하는 것이지, 전송 횟수까지 보장하는 것은 아니다. 그야 말로 스트리밍 송수신이다. 따라서 반드시 송수신자 간에 패킷의 완료 여부를 알 수 있도록 사인을 해 두어야 한다. 보통 패킷의 앞이나 뒤에 이를 확인할 수 있도록 구조를 잡는다. 다음의 의사 코드(pseudo code)는 일반적으로 소켓 수신을 처리하는 방법을 보여준다.

// TCP 수신 처리 방법
ret = recv( s, buf, sizeof( buf ), 0 );
if ( ret <= 0 ) // ret 에러 처리

// 패킷이 잘려 올 수 있기 때문에 이전 패킷과 합친다.
queue.add( buf, ret );

// 패킷이 뭉쳐 올 수도 있으므로 완료 패킷이 없어질 때까지 반복한다.
while ( queue.has_completion_packet() ){
process_completion_packet( queue.get_completion_packet() );
// 처리한 패킷은 큐에서 삭제한다.
queue.remove_completion_packet();}

또 다른 주의해야 할 점은 send, recv 함수의 리턴 값을 명확히 처리해 두는 것이다. 넌블러킹 소켓에서 send 호출은 우리가 생성한 버퍼(스택 또는 힙)를 커널 버퍼(소켓 버퍼)로 복사하고 커널 버퍼에 복사된 크기를 리턴한다. 이 때 커널 버퍼의 공간이 부족하여 요청한 크기와 리턴된 크기가 다를 수 있는데, 이런 경우 보통 네트워크 지연으로 판단해 접속을 끊거나, 사용자가 만든 송신 큐에 임시로 보관해 두고 다음 송신이 가능해졌을 때 재전송하는 방법으로 해결한다. recv 함수는 보통 수신된 패킷 크기를 리턴하며, 0을 리턴하는 경우 정상적으로 접속이 종료되었다는 것으로 볼 수 있다. 단, 서버가 강제로 접속을 끊는 경우 recv는 SOCKET_ERROR를 리턴하면서 GetLastError() 함수로 WSAECONNRESET와 같은 에러 코드를 얻을 수 있다. 에러를 처리해 두면 send, recv 함수가 왜 실패했는지 명확해지기 때문에, 이후 네트워크 에러가 발생했을 때 어떻게 대처해야 할 것인가는 어렵지 않게 판단할 수 있다.

int shutdown(
SOCKET s,
int how
);
int closesocket(
SOCKET s
);


서버 프로그래밍을 할 때 주의해야 할 부분 중 하나가 소켓을 닫을 때의 처리이다. 안전하게 종료하기 위해서는 모든 데이터를 전송한 뒤 접속을 끊으려 할 때 shutdown 호출을 사용해 이 사실을 상대방에게 알려줘야 한다. 물론 상대방도 마찬가지다. 이러한 처리를 Graceful Closure라고 하며 <표 1>처럼 종료 처리를 한다(MSDN의 Graceful Shutdown, Linger Options, and Socket Closure 참조).

 <1> Graceful Closure 과정
클라이언트 측 서버 측
shutdown( s, SD_SEND ) 호출
- 상대 호스트에 FIN 세그먼트 전송
 
  FD_CLOSE 통보 받음
  남은 데이터 모두 전송
서버로부터 받은 데이터 처리 shutdown( s, SD_SEND ) 호출
- 상대 호스트에 FIN 세그먼트 전송
⑤ FD_CLOSE 통보 받음 closesocket 호출
closesocket 호출  


<표 1>과 같이 shutdown을 사용하면 남아 있는 데이터를 보낼 기회를 제공함으로서 소켓의 연결 종료를 제어할 수 있다. 그런데 아직 한 가지 고려해 볼 문제가 남아 있다. shutdown이나 closesocket 모두 ACK(TCP Handshake)를 확인하지 않고 리턴한다는 점이다. 그렇다면 어떻게 우리가 전송한 데이터가 정말로 보내졌는지 확인할 수 있을까?

Graceful Closure 설명을 하면서 Linger 옵션에 관한 설명을 빠뜨릴 수 없다. Linger 옵션은 closesocket 호출로 소켓이 닫히면서 남아 있는 데이터 전송을 어떻게 다룰 것인가를 설정한다. TCP는 상대방으로부터 보낸 패킷에 대한 ACK를 받아야 전송이 완료된 것으로 간주한다. <표 1>에서 서버 측을 보면 상대방으로부터 FIN 세그먼트를 확인한 뒤 데이터를 보내고 shutdown 호출 후 closesocket 호출로 마침내 소켓을 닫는다. 정상적인 과정이지만 내부적으로 FIN ACK는 물론 이전에 전송한 데이터조차 ACK를 받지 못했을 가능성이 있다. 다행히도 시스템은 기본적으로 소켓이 닫힌 후의 클로징 핸드세이크를 처리할 시간(2MSL)을 준다.

Linger 옵션은 이 시간을 조절할 수 있게 하는데 일반적인 경우에 Linger 옵션을 설정할 필요는 없다. Linger를 설정하는 경우 블러킹 소켓에선 closesocket 호출시 블럭될 수 있고, 넌블러킹 소켓은 closesocket에서 WSAEWOULDBLOCK을 리턴하므로 완료되기까지 수차례 호출해야 한다는 단점이 있다. 간혹 이 시간을 0으로 설정하기도 하는데 이것을 핸드 클로저(Hand Closure)라고 하며, 이 때 서버는 closesocket 즉시 해당 소켓에 관한 모든 리소스를 반납한다. 이 경우 상대방은 모든 데이터를 수신하지 못한 채 WSAECONNRESET 에러를 받기 때문에 특별한 경우가 아니라면 권장하지 않는다. <표 2>는 MSDN에서 발췌한 것으로 Linger 옵션에 따른 closesocket 작동 방식을 나타낸다.

 <2> linger 옵션에 따른 closesocket 작동 방식
Option Interval Type of Close Wait for Close?
SO_DONTLINGER Do not care Graceful No
SO_LINGER Zero Hard No
SO_LINGER Nonzero Graceful Yes

<표 2> Iinger 옵션에 따른 closesocket 방식

오버랩드 I/O
오버랩드(overlapped) I/O란 문자 그대로 중첩된 입출력을 뜻한다. CPU에 비해 디스크나 통신 디바이스의 입출력에 걸리는 속도는 대단히 느리기 때문에 오버랩드 I/O를 사용해 디바이스 입출력시에 걸리는 시간 지연을 피할 수 있다. 물론 윈속은 이미 여러 가지 비동기 입출력 방법을 제공하고 있어, 굳이 오버랩드 I/O를 사용하지 않더라도 거의 같은 성능의 비동기 입출력을 구현할 수 있다. 잠시 후 소개할 IOCP(IO Completion Port)와 함께 사용되기 때문에 한 번쯤 거쳐야 할 관문 정도로만 생각해 두고 부담없이 진행해 나가도록 하자. send, recv 대신 WSASend, WSARecv를 사용해 오버랩드 I/O를 할 수 있다.

<리스트 4> WSASend. WSARecv 함수
int WSASend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE
lpCompletionRoutine
);
int WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE
lpCompletionRoutine
);


함수 파라미터의 구체적인 사용법은 이후에 논하기로 하고, 지금은 WSAOVERLAPPED 구조체를 사용해 함수를 호출한다는 것 정도만 알아두자. 넌블러킹 소켓에서와 마찬가지로 오버랩드를 사용한 WSASend, WSARecv 호출은 특별한 이상이 없는 한 WSAEWOULDBLOCK을 리턴한다. 오버랩드 I/O의 완료 여부를 확인하려면 다음의 함수를 호출하면 된다.

BOOL WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags
);

사실은 바로 IOCP를 설명해도 되지만, 오버랩드 I/O를 설명하면서 그냥 지나치면 WSAGetOverlappedResult가 섭섭해 할까봐 한번 등장시켜 보았다. 바로 뒤에 설명할 IOCP를 사용해 완료 통보를 받게 되면, 더 이상 이 함수는 설 자리가 없어지기 때문에 독자의 기억 속에 그리 오래 머물 것 같진 않다. 오버랩드 I/O의 다양한 쓰임새나 윈속의 다른 비동기 입출력 방법에 대해 자세히 알고 싶다면, 마이크로소프트 프레스의 「Network Programming for Microsoft Windows」를 참고하기 바란다.
 



윈속 프로그래밍 리뷰 -3
박범진  (oranze@wemade.com)
디바이스 입출력 완료 통보 포트, IOCP
IOCP는 디바이스의 입출력 완료를 통보하기 위한 포트로서, 빠른 입출력 통보 외에 최적화된 쓰레드 풀링 기술을 포함하고 있다. 디바이스와 IOCP를 연결하는 데 개수 제한이 없고, 최적화된 쓰레드 풀링을 통해 고성능 서버를 구축하는데 큰 도움이 되기 때문에, 현재 많은 윈도우 서버 프로그래머들의 사랑을 받고 있는 귀여운 녀석이기도 하다. 제공되는 성능에 비해 사용법 자체는 의외로 간단해 프로그래머는 IOCP를 만들고, 적절한 수의 워커 쓰레드를 생성한 다음 입출력 완료 통보를 기다리기만 하면 된다.

HANDLE CreateIoCompletionPort (
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);

IOCP를 만들어 주는 좀 웃기는(?) 함수다. 이 함수는 사실상 두 가지 역할을 하는데, 하나는 이름 그대로 IOCP를 생성하는 것이고(네 번째 파라미터만 사용), 다른 하나는 오버랩드 속성을 지닌 소켓과 IOCP를 연결하는 것이다(앞의 세 파라미터만 사용). 「Programming Server-Side Applications for Microsoft Windows 2000」의 저자 제프리 리처(Jeffrey Richter)도 언급한 것이지만, 함수를 왜 저렇게 만들어 놨는지 도저히 이해되지 않는 부분이다. 어쨌든 꽤 중요한 함수이기에 다음의 일련의 흐름을 보면서 IOCP 체계를 확실히 이해해 둘 필요가 있다.

ꊱ IOCP를 만든다
HANDLE h = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0);

처음엔 당연히 IOCP를 만들어야 한다. 먼저 IOCP를 만들 때는 앞의 세 파라미터가 쓰이지 않으므로 가볍게 INVALID_HANDLE_VALUE, 0, 0을 넘겨주자. NumberOfConcurrentThreads에 오버랩드 I/O를 처리하기 위해 동시에 실행할 수 있는 쓰레드의 수를 지정하는데, 0을 넘기면 시스템은 설치된 프로세서(CPU)의 수만큼 할당한다.

ꊲ IOCP를 감시할 쓰레드를 생성한다
SYSTEM_INFO si
GetSystemInfo( &si );

numThreads = si.dwNumberOfProcessors * 2;

for ( i = 0; i < numThreads; i++ )
_beginthreadex( NULL, 0, WorkerThread, ... );

IOCP의 완료 통보를 받을 쓰레드를 생성한다. 좀 이상한 부분이 눈에 띄지 않는가? IOCP를 만들 때 CPU 수만큼의 쓰레드가 동시에 돌아갈 수 있도록 그 수를 제한해 놓고선, 정작 쓰레드는 그 두 배만큼 만들고 있다. 이는 워크 쓰레드가 Wait 상태에 다다를 때(예 : Sleep 호출) IOCP가 또 다른 쓰레드에 완료 통보를 해주기 때문에 여분의 쓰레드를 미리 만들어 두는 것이다. 두 배라고 한 것은 필자 맘대로 정한 수치이고 서버의 구현 방법이나 서비스 내용에 따라 적절한 값을 찾는 것이 좋다.

ꊳ 소켓과 IOCP를 연결시킨다
CreateIoCompletionPort( (HANDLE) my_socket, iocp_handle, completion_key, 0 )

오버랩드 I/O를 IOCP로 통보받기 위해 소켓 핸들과 IOCP 핸들을 연결시켜야 한다. 세 번째 파라미터인 completion_key는 나중에 오버랩드 I/O에 대한 완료 통보를 받을 때, 어떤 소켓으로부터의 완료 통보인지 식별할 수 있게 해주는 것으로 보통 소켓을 포함하고 있는 객체의 주소를 넘긴다. 그리고 앞서 언급했듯 마지막 파라미터는 쓰지 않는다.

ꊴ IOCP를 감시(+_+)한다
WorkerThread()
{
while ( TRUE )
{
GetQueuedCompletionStatus(
iocp_handle, // HANDLE CompletionPort
&bytes_transferred, // LPDWORD lpNumberOfBytes
&completion_key, // PULONG_PTR lpCompletionKey
&overlapped, // LPOVERLAPPED *lpOverlapped
INFINITE ); // WORD dwMilliseconds

// completion_key와 오버랩드를 보면
// 어떤 소켓의 오버랩드 I/O인지 구별할 수 있다.
}
};

처음 보는 함수가 나타났다. 이미 독자도 예상하고 있겠지만 GetQueuedCompletionStatus가 IOCP의 부름(Thread Wake-Up)을 받기 위해 기다리고 있다. 이 함수를 통해 어떤 소켓의 어떤 호출인지, 또 얼마만큼 전송이 되었고 에러 코드는 무엇인지 등을 확인할 수 있다.

ꊵ WSASend, WSARecv 등의 오버랩드 I/O를 시작한다
WSASend(
s, &wsabuf, 1,
&bytes_transferred, 0, &overlapped, NULL );

WSARecv(
s, &wsabuf, 1,
&bytes_transferred, &flag, &overlapped, NULL );

패킷을 주고받기 위해 오버랩드 구조체를 이용한다. WSASend, WSARecv 각각의 파라미터에 주의를 기울일 필요가 있는데, 이에 관한 자세한 설명은 다음 번에 직접 네트워크 라이브러리를 구현하면서 자세히 설명하기로 하고 지금은 IOCP 체계를 이해하는 것에 초점을 맞추자.






두 개의 CPU가 설치된 윈도우 2000에서 <그림 2>와 같은 IOCP 서버가 실행 중이라고 가정해 보자. IOCP를 만들 때 NumberOfConcurrentThreads에 0을 넘겨 동시 쓰레드(concurrent thread)의 수가 두 개가 되도록 했다. #1은 이미 완료 통보를 받아 해당 객체의 송수신을 처리 중이고, #2가 지금 막 완료 통보를 받고 있다. 이렇게 되면 정확히 두 개의 쓰레드가 동시에 실행중인 것이며, IOCP 큐에 완료 통보가 도착하더라도 IOCP는 다른 쓰레드(#3)에 완료 통보를 하지 않는다. 이 시점에서 발생할 수 있는 두 가지 시나리오를 세워 보았다.

◆ 시나리오 1 - #1이 완료 통보 처리를 마침
완료 처리가 끝났기 때문에, #1은 다시 GetQueuedCompletionStatus 함수를 호출한다. 이때 IOCP는 큐에 쌓여 있던 다른 완료 통보를 다시 #1에 넘겨준다. 먼저 기다리고 있던 #3에 넘기지 않는 이유는 쓰레드 컨텍스트 스위칭을 줄이기 위해서다.

◆ 시나리오 2 - #1이 처리 도중 Sleep을 호출
프로그래머가 무슨 생각으로 Sleep을 호출했는지는 모르겠지만 어쨌든 쓰레드 Wait 상태에 돌입한다. 이 때 기다리고 있던 #3이 IOCP로부터 완료 통보를 받는다. 이 시점의 실제 동시 쓰레드 수는 2+1(Wait State)이며, #1이 잠에서 깨어날 경우 순간적으로 IOCP를 만들 때 지정했던 동시성 수의 범위를 초과할 수 있다. 이후 IOCP는 다시 동시 쓰레드의 수가 2가 되도록 조절한다. 이러한 이유로 IOCP 생성시에 지정해 준 NumberOfConcurrentThreads의 수보다 실제로 많은 워커 쓰레드를 생성하는 것이다.

IOCP를 이용한 서버 구현시 주의사항
많은 개발자들이 범하는 대부분의 실수는 멀티 쓰레드와 비동기 입출력의 부족한 이해에 기인한다. 멀티 쓰레드 프로그래밍만 하더라도 어렵고 복잡한데, 여기에 비동기 입출력까지 더해지니 네트워크 개발자들이 겪을 그 혼란은 충분히 짐작할 만하다. 이번엔 IOCP를 이용해 서버 네트워크 코드를 구현할 때 특히 주의해야 점을 알아보기로 하자.

에러 코드를 반드시 확인한다
WSASend, WSARecv 등을 통해 오버랩드 I/O를 할 때 정상적인 경우 WSAEWOULDBLOCK을 리턴한다. 그러나 원격 호스트가 접속을 끊거나(WSAECONNRESET), 가상 회선에 문제가 발생했을 때(WSAECONNABORTED)와 같은 문제는 빈번히 발생한다. 이 경우 별 수 없이 이쪽에서도 접속을 끊는 수밖에 없다. 골치 아픈 부분은 WSAENOBUFS와 같은 에러를 만나는 경우다. 다음 호에서 구현을 통해 자세히 알아보겠지만, 시스템 리소스(커널 리소스) 제한에 걸리게 되면 오버랩드 I/O는 ‘WSAENOBUFS 에러’를 내뱉으며 실패한다. 마찬가지로 그냥 접속을 끊으면 되는 것 아니냐? 라고 반문하겠지만, 그것이 클라이언트가 아니라 대량의 클라이언트가 접속한 상황에서의 서버간 송수신에서 발생하는 것이라면 더욱 심각해진다. 대량의 클라이언트가 접속한 상황에서는 언제든지 시스템 리소스가 바닥날 수 있기 때문에 클라이언트의 연결을 적절히 분산시킬 수 있는 메커니즘이 필요하며, 불가피한 경우 클라이언트의 접속을 제한해야 한다.

참조 카운트를 유지한다
오버랩드 호출을 걸어두고, 완료 통보를 받기도 전에 오버랩드 버퍼나 소켓 객체가 삭제되어서는 안된다. 또한 한 객체에 대해 둘 이상의 오버랩드 호출이 있는 경우엔 반드시 참조 카운트를 유지해야 하며, 객체를 제거해야 하는 경우에 이 참조 카운트가 0인지 확인해야 한다. 참조 카운트를 유지하지 않고 완료 통보가 아직 더 남아있는 상태에서 객체를 삭제하면, 당연한 것이지만 그 다음 완료 통보시 엉뚱한 메모리 위치를(IOCP로 말하자면 CompletionKey나 OverlappedPointer) 가리켜 크래시를 발생시킨다. 원인을 모르고 객체가 삭제된 것에 분개해 정적 메모리 관리 등으로 당장 급한 불을 끄는 것은 근본적인 해결책이 될 수 없다.

데드락을 주의한다
IOCP의 워커 쓰레드만을 이용해 서비스 코드를 구현할 때 주의해야 할 사항이 있다. 주로 샘플 소스로 쓰이는 에코(echo) 서버나, 실제로 IOCP로 구현되어 있는 IIS(Internet Information Server)와 같은 서버는 객체간 상호 참조가 발생되지 않아 이러한 문제는 없다. 그러나 채팅 서버와 같은 상호 참조(즉, 한 객체가 다른 객체에 직접적인 접근이 일어나는 것)가 발생하는 서비스에서는 양방향 상호 참조가 동시에 일어나는 경우에 데드락(dead-lock)이 발생할 수 있다. 따라서 동기화에 각고의 노력을 기울여야 하며, 이것보다는 패킷을 처리하는 전용 쓰레드를 따로 두어 일괄적으로 처리하는 방법을 권한다.

다양한 의견 기다리며
이번 호에서는 본격적인 구현에 앞서 필요한 내용들을 쭉 살펴봤다. 지면 관계상 조금 빠르게 진행된 감이 있는데 부족한 부분은 참고자료를 살펴보기 바란다. 필자도 부족한 부분이 많기 때문에 오해하고 있는 부분이 있거나, 잘못된 코드를 제공할 수 있다. 이런 부분이 발견되면 즉시 연락해 바로잡을 수 있도록 도와주길 바란다. 그리고 이번 기사에 대한 질책이나 조언, 다양한 의견을 접할 수 있다면 앞으로 좋은 기사를 쓰는데 큰 도움이 될 것이다. 부담없이 연락해 주길....



















서버 프로그래밍을 하다 보면 정말 웃지 못할 사건들을 많이 접하게 된다. 클라이언트 접속이 1000개까지는 문제가 없다가 1001번째에 갑자기 크래시(crash)가 발생하기도 하고, 서버를 구동시킨 지 2~3일이 지난 시점부터 조금씩 리소스가 새기도 한다. 부지런한 개발자들은 MSDN의 버그 리포트와 테크니컬 기사를 샅샅이 뒤져보며 운영체제 또는 하드웨어의 버그이길 간절히 바란다.
그러나 아쉽게도 이런 경우의 대부분이 본인의 실수에서 비롯되므로 유용한 단서를 찾기는 힘들다. 몇몇 뛰어난 프로그래머들은 몇 날 며칠을 밤새워 고생하다 갑자기 무언가 드디어 알아챘다는 듯 멀쩡히 죄 없는 PC를 포맷하기도 한다(필자도 과거에 일말의 희망을 걸고 수차례 포맷해 본 경험이 있다. 물론 다시 설치한다고 해결될 성질의 것이 결코 아니었다). 앞으로의 설명은 서버의 성능이나 최적화 문제보다는 안정적이고 유연한 네트워킹 환경을 구축하는 것에 초점을 맞춰 진행할 것이다.

소켓 API 리뷰
네트워크 개발자들이 겪는 문제들의 많은 부분이 소켓과 TCP의 특성을 제대로 이해하지 못한 것에서 시작한다. 다소 지루할 수도 있겠지만, 먼저 각 소켓 API의 중요한 점을 되짚어 보는 것으로 시작하겠다.
소켓을 생성하는 함수는 두 가지가 있다. <리스트 1>처럼 WSA Socket 쪽이 좀더 다양한 프로토콜을 열거할 수 있다. 하지만 우리가 만들 서버는 인터넷 TCP 프로토콜만을 사용하기 때문에 어느 것을 사용해도 문제없다. 단 WSASocket으로 소켓을 생성하는 경우 dwFlags 파라미터에 WSA_FLAG_OVERLAPPED를 넘겨 오버랩드 속성을 가지도록 해야 한다. 그렇지 않으면 이후 다룰 WSASend, WSARecv 등의 오버랩드 호출은 무시될 것이다. 오버랩드 I/O에 관한 내용은 추후 자세히 설명하겠다.

int bind(
SOCKET s,
const struct sockaddr FAR *name,
int namelen
);

빈 소켓만으로는 아무 것도 할 수 없다. 소켓과 로컬 주소를 연결시킨 뒤에야 비로소 네트워크 통신을 할 수 있는데, bind가 이러한 역할을 해 준다. 먼저 name 파라미터를 자세히 살펴보자(<리스트 2>).
sockaddr은 bind할 주소를 지정하는데 쓰이는 16바이트 크기의 구조체다. 소켓에는 다양한 주소 패밀리(AF_UNIX, AF_INET, AF_IMPLINK, ...)와 각각의 하위 프로토콜이 존재한다. 각 주소 패밀리에 따라 주소 지정 방법이 다를 수 있는데, 우리는 인터넷 프로토콜(AF_INET)을 사용하므로 AF_INET의 주소 지정을 쉽게 하기 위해 우측의 sockaddr_in을 사용한다.
보통 sockaddr_in의 sin_addr 필드에 ADDR_ANY를 집어넣는데, 이것은 멀티홈드 호스트(예 : 여러 LAN 카드가 꽂혀 있는 호스트)의 특정 네트워크 주소를 선택하지 않겠다는 뜻이다. 그러나 성능이나 보안 측면을 강화시키기 위해 특정 네트워크의 주소를 입력할 수 있다.
윈속이 제공하는 Name Resolution 함수 중 하나인 gethostb yname를 사용해 로컬 호스트의 네트워크를 열거할 수 있다.

HOSTENT *he = gethostbyname( host_name );
he->h_addr_list[0]; // 첫 번째(예 : LAN CARD #1)
he->h_addr_list[1]; // 두 번째(예 : LAN CARD #2)
he->h_addr_list[2]; // 세 번째(예 : CABLE MODEM)

다음으로 sockaddr_in의 포트를 살펴보자. 이 값이 0이면 시스템은 적당한 포트를 찾아 맵핑해 준다. 윈도우 2000에서의 기본 값은 1024~5000 사이의 값인데, 부족할 경우 TCP/IP 관련 레지스트리 키(MaxUserPort)의 최대 값을 변경할 수 있다.

MaxUserPort
Key: HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesTcpipParameters
Value Type: REG_DWORDmaximum port number
Valid Range: 500065534 (decimal)
Default: 0x1388 (5000 decimal)

이외에도 MaxFreeTcbs와 MaxHashTableSize 등을 조절해 맵핑될 소켓 수를 조절할 수 있다. 관심있는 독자는 「Microsoft Win dows 2000 TCP/IP Implementation Details」를 참고하기 바란다.
그리고 당연하겠지만 같은 주소로의 두 번째 bind는 실패한다. 간혹, 이전에 bind된 소켓을 분명히 닫았음에도 두 번째 bind가 실패하는 경우가 있는데, 이것은 이전 소켓이 실제로 완전히 닫히지 않고 TIME_WAIT 상태에 머물러 있기 때문이다. 서버를 재시작하는 경우에도 발생할 수 있는데, 이런 상황을 피하려면 setsockopt 함수를 사용해 SO_REUSEADDR 옵션을 셋팅하면 된다. TCP 상태에 대해 잘 모르고 있다면 TCP/IP 서적 등을 참고해 반드시 숙지하기 바란다.

int connect(
SOCKET s,
const struct sockaddr FAR *name,
int namelen
);

상대방 호스트에 접속하기 위해 connect를 호출한다. connect를 호출하기 위해 bind할 필요는 없다. 소켓을 생성한 뒤 바로 connect를 호출하면 자동으로 시스템이 지정하는 포트로 bind되는데, 이 역시 1024~5000 사이의 값을 가진다. 스트레스 테스트(stress test)를 하면 이 정도의 연결이 부족할 수 있다. 따라서 필요하다면 앞서 언급한대로 레지스트리 값을 적당한 값으로 설정해 주자.

int listen(
SOCKET s,
int backlog
);

listen은 소켓에 TCP 접속을 받아들일 수 있는 속성을 부여해 준다. backlog 파라미터는 동시에 접속이 몰렸을 때를 처리하기 위한 큐의 크기인데, 보통 시스템의 최대 값을 지정해서 쓴다. 윈속 2.0 이전 버전에서 이 값의 최대 값은 5였는데, 이것은 접속 요청이 최대 5개까지 큐될 수 있다는 것을 뜻한다. 윈속 2가 등장하면서 SO MAXCONN이라는 상수 값을 사용하는데, 내부적으로 윈도우 2000 서버는 200개, 프로는 5개까지 설정된다. 접속 처리를 위해 accept를 호출하면, backlog 큐의 첫 번째 노드가 삭제되면서 다른 접속 요청을 큐에 넣을 수 있다. backlog 큐가 가득 차면 클라이언트의 connect 호출은 WSAECONNREFUSED 에러를 리턴한다.

SOCKET accept(
SOCKET s,
struct sockaddr FAR *addr,
int FAR *addrlen
);

accept는 서버 소켓의 접속 큐에서 첫 번째 노드를 가져와 소켓을 생성한 뒤 리턴한다. 리턴된 소켓은 s 파라미터와 동일한 속성을 가진다는 것을 기억해 두자.
TCP 패킷을 주고받을 때 사용한다. 가장 빈번하게 호출되는 함수인 만큼 네트워크 개발자들이 주의해야 하는 부분이다. TCP는 신뢰할 수 있는(reliable) 스트림 기반의 프로토콜이다. 여기서 스트림 기반이라는 것에 주목할 필요가 있다. 수신자는 언제나 송신자가 전송한 만큼 받기 마련이지만, 이것이 곧 send, recv 함수의 호출 횟수까지 같다는 것을 뜻하지는 않는다.
전송된 패킷은 인터넷의 수많은 게이트웨이를 경유하면서 상대방에게 도착하는데, send 호출과는 관계없이 패킷이 뭉쳐오기도 하고 완전히 조각난 상태로 도착하기도 한다. TCP는 보낸 순서대로 끝까지 도착하는 것을 보장하는 것이지, 전송 횟수까지 보장하는 것은 아니다. 그야말로 스트리밍 송수신이다. 따라서 반드시 송수신자 간에 패킷의 완료 여부를 알 수 있도록 사인을 해 두어야 한다. 보통 패킷의 앞이나 뒤에 이를 확인할 수 있도록 구조를 잡는다. 다음의 의사 코드(pseudo code)는 일반적으로 소켓 수신을 처리하는 방법을 보여준다.

// TCP 수신 처리 방법
ret = recv( s, buf, sizeof( buf ), 0 );
if ( ret <= 0 ) // ret 에러 처리

// 패킷이 잘려 올 수 있기 때문에 이전 패킷과 합친다.
queue.add( buf, ret );

// 패킷이 뭉쳐 올 수도 있으므로 완료 패킷이 없어질 때까지 반복한다.
while ( queue.has_completion_packet() ){
process_completion_packet( queue.get_completion_packet() );
// 처리한 패킷은 큐에서 삭제한다.
queue.remove_completion_packet();}

또 다른 주의해야 할 점은 send, recv 함수의 리턴 값을 명확히 처리해 두는 것이다. 넌블러킹 소켓에서 send 호출은 우리가 생성한 버퍼(스택 또는 힙)를 커널 버퍼(소켓 버퍼)로 복사하고 커널 버퍼에 복사된 크기를 리턴한다. 이 때 커널 버퍼의 공간이 부족하여 요청한 크기와 리턴된 크기가 다를 수 있는데, 이런 경우 보통 네트워크 지연으로 판단해 접속을 끊거나 사용자가 만든 송신 큐에 임시로 보관해 두고 다음 송신이 가능해졌을 때 재전송하는 방법으로 해결한다. recv 함수는 보통 수신된 패킷 크기를 리턴하며, 0을 리턴하는 경우 정상적으로 접속이 종료되었다는 것으로 볼 수 있다. 단, 서버가 강제로 접속을 끊는 경우 recv는 SOCKET_ERROR를 리턴하면서 GetLastError() 함수로 WSAECONNRESET와 같은 에러 코드를 얻을 수 있다. 에러를 처리해 두면 send, recv 함수가 왜 실패했는지 명확해지기 때문에, 이후 네트워크 에러가 발생했을 때 어떻게 대처해야 할 것인가는 어렵지 않게 판단할 수 있다.

int shutdown(
SOCKET s,
int how
);
int closesocket(
SOCKET s
);

서버 프로그래밍을 할 때 주의해야 할 부분 중 하나가 소켓을 닫을 때의 처리이다. 안전하게 종료하기 위해서는 모든 데이터를 전송한 뒤 접속을 끊으려 할 때 shutdown 호출을 사용해 이 사실을 상대방에게 알려줘야 한다. 물론 상대방도 마찬가지다. 이러한 처리를 Graceful Closure라고 하며 <표 1>처럼 종료 처리를 한다(MSDN의 Graceful Shutdown, Linger Options, and Socket Closure 참조).
<표 1>과 같이 shutdown을 사용하면 남아 있는 데이터를 보낼 기회를 제공함으로써 소켓의 연결 종료를 제어할 수 있다. 그런데 아직 한 가지 고려해 볼 문제가 남아 있다. shutdown이나 closesocket 모두 ACK(TCP Handshake)를 확인하지 않고 리턴한다는 점이다. 그렇다면 어떻게 우리가 전송한 데이터가 정말로 보내졌는지 확인할 수 있을까?
Graceful Closure 설명을 하면서 Linger 옵션에 관한 설명을 빠뜨릴 수 없다. Linger 옵션은 closesocket 호출로 소켓이 닫히면서 남아 있는 데이터 전송을 어떻게 다룰 것인가를 설정한다. TCP는 상대방으로부터 보낸 패킷에 대한 ACK를 받아야 전송이 완료된 것으로 간주한다. <표 1>에서 서버 측을 보면 상대방으로부터 FIN 세그먼트를 확인한 뒤 데이터를 보내고 shutdown 호출 후 closesocket 호출로 마침내 소켓을 닫는다. 정상적인 과정이지만 내부적으로 FIN ACK는 물론 이전에 전송한 데이터조차 ACK를 받지 못했을 가능성이 있다. 다행히도 시스템은 기본적으로 소켓이 닫힌 후의 클로징 핸드세이크(Closing Handshake)를 처리할 시간(2MSL)을 준다.
Linger 옵션은 이 시간을 조절할 수 있게 하는데 일반적인 경우에 Linger 옵션을 설정할 필요는 없다. Linger를 설정하는 경우 블러킹 소켓에선 closesocket 호출시 블럭될 수 있고, 넌블러킹 소켓은 closesocket에서 WSAEWOULDBLOCK을 리턴하므로 완료되기까지 수차례 호출해야 한다는 단점이 있다. 간혹 이 시간을 0으로 설정하기도 하는데 이것을 하드 클로저(Hard Closure)라고 하며, 이 때 서버는 closesocket 즉시 해당 소켓에 관한 모든 리소스를 반납한다. 이 경우 상대방은 모든 데이터를 수신하지 못한 채 WSAECON NRESET 에러를 받기 때문에 특별한 경우가 아니라면 권장하지 않는다. <표 2>는 MSDN에서 발췌한 것으로 Linger 옵션에 따른 closesocket 작동 방식을 나타낸다.

오버랩드 I/O
오버랩드(overlapped) I/O란 문자 그대로 중첩된 입출력을 뜻한다. CPU에 비해 디스크나 통신 디바이스의 입출력에 걸리는 속도는 대단히 느리기 때문에 오버랩드 I/O를 사용해 디바이스 입출력시에 걸리는 시간 지연을 피할 수 있다. 물론 윈속은 이미 여러 가지 비동기 입출력 방법을 제공하고 있어, 굳이 오버랩드 I/O를 사용하지 않더라도 거의 같은 성능의 비동기 입출력을 구현할 수 있다. 잠시 후 소개할 IOCP(IO Completion Port)와 함께 사용되기 때문에 한 번쯤 거쳐야 할 관문 정도로만 생각해 두고 부담없이 진행해 나가도록 하자. send, recv 대신 WSASend, WSARecv를 사용해 오버랩드 I/O를 할 수 있다.
함수 파라미터의 구체적인 사용법은 이후에 논하기로 하고, 지금은 WSAOVERLAPPED 구조체를 사용해 함수를 호출한다는 것 정도만 알아두자. 넌블러킹 소켓에서와 마찬가지로 오버랩드를 사용한 WSASend, WSARecv 호출은 특별한 이상이 없는 한 WSAE WOULDBLOCK을 리턴한다. 오버랩드 I/O의 완료 여부를 확인하려면 다음의 함수를 호출하면 된다.

BOOL WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags
);

사실은 바로 IOCP를 설명해도 되지만, 오버랩드 I/O를 설명하면서 그냥 지나치면 WSAGetOverlappedResult가 섭섭해 할까봐 한번 등장시켜 보았다. 바로 뒤에 설명할 IOCP를 사용해 완료 통보를 받게 되면, 더 이상 이 함수는 설 자리가 없어지기 때문에 독자의 기억 속에 그리 오래 머물 것 같진 않다. 오버랩드 I/O의 다양한 쓰임새나 윈속의 다른 비동기 입출력 방법에 대해 자세히 알고 싶다면, 마이크로소프트 프레스의 「Network Programming for Microsoft Windows」를 참고하기 바란다.

디바이스 입출력 완료 통보 포트, IOCP
IOCP는 디바이스의 입출력 완료를 통보하기 위한 포트로서, 빠른 입출력 통보 외에 최적화된 쓰레드 풀링 기술을 포함하고 있다. 디바이스와 IOCP를 연결하는 데 개수 제한이 없고, 최적화된 쓰레드 풀링을 통해 고성능 서버를 구축하는 데 큰 도움이 되기 때문에, 현재 많은 윈도우 서버 프로그래머들의 사랑을 받고 있는 귀여운 녀석이기도 하다. 제공되는 성능에 비해 사용법 자체는 의외로 간단해 프로그래머는 IOCP를 만들고, 적절한 수의 워커 쓰레드를 생성한 다음 입출력 완료 통보를 기다리기만 하면 된다.

HANDLE CreateIoCompletionPort (
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);

IOCP를 만들어 주는 좀 웃기는(?) 함수다. 이 함수는 사실상 두 가지 역할을 하는데, 하나는 이름 그대로 IOCP를 생성하는 것이고(네 번째 파라미터만 사용), 다른 하나는 오버랩드 속성을 지닌 소켓과 IOCP를 연결하는 것이다(앞의 세 파라미터만 사용). 「Program ming Server-Side Applications for Microsoft Windows 2000」의 저자 제프리 리처(Jeffrey Richter)도 언급한 것이지만, 함수를 왜 저렇게 만들어 놨는지 도저히 이해되지 않는 부분이다. 어쨌든 꽤 중요한 함수이기에 다음의 일련의 흐름을 보면서 IOCP 체계를 확실히 이해해 둘 필요가 있다.

짾 IOCP를 만든다

HANDLE h = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );

처음엔 당연히 IOCP를 만들어야 한다. 먼저 IOCP를 만들 때는 앞의 세 파라미터가 쓰이지 않으므로 가볍게 INVALI D_HANDLE_ VALUE, 0, 0을 넘겨주자. NumberOfConcurrent Threads에 오버랩드 I/O를 처리하기 위해 동시에 실행할 수 있는 쓰레드의 수를 지정하는데, 0을 넘기면 시스템은 설치된 프로세서(CPU)의 수만큼 할당한다.

짿 IOCP를 감시할 쓰레드를 생성한다

SYSTEM_INFO si
GetSystemInfo( &si );

numThreads = si.dwNumberOfProcessors * 2;

for ( i = 0; i < numThreads; i++ )
_beginthreadex( NULL, 0, WorkerThread, ... );

IOCP의 완료 통보를 받을 쓰레드를 생성한다. 좀 이상한 부분이 눈에 띄지 않는가? IOCP를 만들 때 CPU 수만큼의 쓰레드가 동시에 돌아갈 수 있도록 그 수를 제한해 놓고선, 정작 쓰레드는 그 두 배만큼 만들고 있다. 이는 워크 쓰레드가 Wait 상태에 다다를 때(예 : Sleep 호출) IOCP가 또 다른 쓰레드에 완료 통보를 해주기 때문에 여분의 쓰레드를 미리 만들어 두는 것이다. 두 배라고 한 것은 필자 맘대로 정한 수치이고 서버의 구현 방법이나 서비스 내용에 따라 적절한 값을 찾는 것이 좋다.

쨁 소켓과 IOCP를 연결시킨다

CreateIoCompletionPort( (HANDLE) my_socket, iocp_handle, completion_key, 0 )

오버랩드 I/O를 IOCP로 통보받기 위해 소켓 핸들과 IOCP 핸들을 연결시켜야 한다. 세 번째 파라미터인 completion_key는 나중에 오버랩드 I/O에 대한 완료 통보를 받을 때, 어떤 소켓으로부터의 완료 통보인지 식별할 수 있게 해주는 것으로 보통 소켓을 포함하고 있는 객체의 주소를 넘긴다. 그리고 앞서 언급했듯 마지막 파라미터는 쓰지 않는다.

쨂 IOCP를 감시(+_+)한다

WorkerThread()
{
while ( TRUE )
{
GetQueuedCompletionStatus(
iocp_handle, // HANDLE CompletionPort
&bytes_transferred, // LPDWORD lpNumberOfBytes
&completion_key, // PULONG_PTR lpCompletionKey
&overlapped, // LPOVERLAPPED *lpOverlapped
INFINITE ); // WORD dwMilliseconds

// completion_key와 오버랩드를 보면
// 어떤 소켓의 오버랩드 I/O인지 구별할 수 있다.
}
};
처음 보는 함수가 나타났다. 이미 독자도 예상하고 있겠지만 Get QueuedCompletionStatus가 IOCP의 부름(Thread Wake-Up)을 받기 위해 기다리고 있다. 이 함수를 통해 어떤 소켓의 어떤 호출인지, 또 얼마만큼 전송이 되었고 에러 코드는 무엇인지 등을 확인할 수 있다.

쨃 WSASend, WSARecv 등의 오버랩드 I/O를 시작한다

WSASend(
s, &wsabuf, 1,
&bytes_transferred, 0, &overlapped, NULL );

WSARecv(
s, &wsabuf, 1,
&bytes_transferred, &flag, &overlapped, NULL );


패킷을 주고받기 위해 오버랩드 구조체를 이용한다. WSASend, WSARecv 각각의 파라미터에 주의를 기울일 필요가 있는데, 이에 관한 자세한 설명은 다음 번에 직접 네트워크 라이브러리를 구현하면서 자세히 설명하기로 하고 지금은 IOCP 체계를 이해하는 것에 초점을 맞추자.
두 개의 CPU가 설치된 윈도우 2000에서 <그림 2>와 같은 IOCP 서버가 실행 중이라고 가정해 보자. IOCP를 만들 때 NumberOf ConcurrentThreads에 0을 넘겨 동시 쓰레드(concurrent thread)의 수가 두 개가 되도록 했다. #1은 이미 완료 통보를 받아 해당 객체의 송수신을 처리 중이고, #2가 지금 막 완료 통보를 받고 있다. 이렇게 되면 정확히 두 개의 쓰레드가 동시에 실행중인 것이며, IOCP 큐에 완료 통보가 도착하더라도 IOCP는 다른 쓰레드(#3)에 완료 통보를 하지 않는다. 이 시점에서 발생할 수 있는 두 가지 시나리오를 세워 보았다.

◆ 시나리오 1 - #1이 완료 통보 처리를 마침
완료 처리가 끝났기 때문에, #1은 다시 GetQueuedCompletion Status 함수를 호출한다. 이때 IOCP는 큐에 쌓여 있던 다른 완료 통보를 다시 #1에 넘겨준다. 먼저 기다리고 있던 #3에 넘기지 않는 이유는 쓰레드 컨텍스트 스위칭을 줄이기 위해서다.

◆ 시나리오 2 - #1이 처리 도중 Sleep을 호출
프로그래머가 무슨 생각으로 Sleep을 호출했는지는 모르겠지만 어쨌든 쓰레드 Wait 상태에 돌입한다. 이 때 기다리고 있던 #3이 IOCP로부터 완료 통보를 받는다. 이 시점의 실제 동시 쓰레드 수는 2+1(Wait State)이며, #1이 잠에서 깨어날 경우 순간적으로 IOCP를 만들 때 지정했던 쓰레드 수의 범위를 초과할 수 있다. 이후 IOCP는 다시 동시에 실행될 쓰레드 수가 2가 되도록 조절한다. 이러한 이유로 IOCP 생성시에 지정해 준 NumberOfConcurrentThreads의 수보다 실제로 많은 워커 쓰레드를 생성하는 것이다.

IOCP를 이용한 서버 구현시 주의사항
많은 개발자들이 범하는 대부분의 실수는 멀티 쓰레드와 비동기 입출력의 이해 부족에 기인한다. 멀티 쓰레드 프로그래밍만 하더라도 어렵고 복잡한데, 여기에 비동기 입출력까지 더해지니 네트워크 개발자들이 겪을 그 혼란은 충분히 짐작할 만하다. 이번엔 IOCP를 이용해 서버 네트워크 코드를 구현할 때 특히 주의해야 점을 알아보기로 하자.

에러 코드를 반드시 확인한다
WSASend, WSARecv 등을 통해 오버랩드 I/O를 할 때 정상적인 경우 WSAEWOULDBLOCK을 리턴한다. 그러나 원격 호스트가 접속을 끊거나(WSAECONNRESET), 가상 회선에 문제가 발생했을 때(WSAECONNABORTED)와 같은 문제는 빈번히 발생한다. 이 경우 별 수 없이 이쪽에서도 접속을 끊는 수밖에 없다. 골치 아픈 부분은 WSAENOBUFS와 같은 에러를 만나는 경우다. 다음 호에서 구현을 통해 자세히 알아보겠지만, 시스템 리소스(커널 리소스) 제한에 걸리게 되면 오버랩드 I/O는 ‘WSAENOBUFS 에러’를 내뱉으며 실패한다. 마찬가지로 ‘그냥 접속을 끊으면 되는 것 아니냐?’고 반문하겠지만, 그것이 클라이언트가 아니라 대량의 클라이언트가 접속한 상황에서의 서버간 송수신에서 발생하는 것이라면 더욱 심각해진다. 대량의 클라이언트가 접속한 상황에서는 언제든지 시스템 리소스가 바닥날 수 있기 때문에 클라이언트의 연결을 적절히 분산시킬 수 있는 메커니즘이 필요하며, 불가피한 경우 클라이언트의 접속을 제한해야 한다.

참조 카운트를 유지한다
오버랩드 호출을 걸어두고, 완료 통보를 받기도 전에 오버랩드 버퍼나 소켓 객체가 삭제돼서는 안된다. 또한 한 객체에 대해 둘 이상의 오버랩드 호출이 있는 경우엔 반드시 참조 카운트를 유지해야 하며, 객체를 제거해야 하는 경우에 이 참조 카운트가 0인지 확인해야 한다. 참조 카운트를 유지하지 않고 완료 통보가 아직 더 남아있는 상태에서 객체를 삭제하면, 당연한 것이지만 그 다음 완료 통보시 엉뚱한 메모리 위치를(IOCP로 말하자면 CompletionKey나 Overlapped Pointer) 가리켜 크래시를 발생시킨다. 원인을 모르고 객체가 삭제된 것에 분개해 정적 메모리 관리 등으로 당장 급한 불을 끄는 것은 근본적인 해결책이 될 수 없다.

데드락을 주의한다
IOCP의 워커 쓰레드만을 이용해 서비스 코드를 구현할 때 주의해야 할 사항이 있다. 주로 샘플 소스로 쓰이는 에코(echo) 서버나, 실제로 IOCP로 구현되어 있는 IIS(Internet Information Server)와 같은 서버는 객체간 상호 참조가 발생되지 않아 이러한 문제는 없다. 그러나 채팅 서버와 같은 상호 참조(즉, 한 객체가 다른 객체에 직접적인 접근이 일어나는 것)가 발생하는 서비스에서는 양방향 상호 참조가 동시에 일어나는 경우에 데드락(dead-lock)이 발생할 수 있다. 따라서 동기화에 각고의 노력을 기울여야 하며, 이것보다는 패킷을 처리하는 전용 쓰레드를 따로 두어 일괄적으로 처리하는 방법을 권한다.

다양한 의견 기다리며
이번 호에서는 본격적인 구현에 앞서 필요한 내용들을 쭉 살펴봤다. 지면 관계상 조금 빠르게 진행된 감이 있는데 부족한 부분은 참고자료를 살펴보기 바란다. 필자도 부족한 부분이 많기 때문에 오해하고 있는 부분이 있거나, 잘못된 코드를 제공할 수 있다. 이런 부분이 발견되면 즉시 연락해 바로잡을 수 있도록 도와주길 바란다. 그리고 이번 기사에 대한 질책이나 조언, 다양한 의견을 접할 수 있다면 앞으로 좋은 기사를 쓰는 데 큰 도움이 될 것이다. 부담없이 연락해 주길..(^^;)

[출처] 윈속 프로그래밍 리뷰


Nagle 알고리즘

Nagle알고리즘? 네트워크 상에 패킷의 수를 줄이기 위해 개발된 알고리즘

 

1. 일반 네트워크 통신방법

- 일반적인 통신알고리즘은 데이터는 패킷으로 만들어 보낸다는 것이며 수신호스트는 이에 대한 ACK를 보낸다는 것입니다. 예를 들어, A,B 두 호스트가 통신을 합니다. A B에게 'Nagle'라는 데이터를 보내기 원하면, 먼저 'N'이라는 데이터를 패킷으로 만들어 출력버퍼로 보냅니다. 그리고 ACK를 받고 안받고 관계없이 'a'를 패킷으로 만들어 보내고 이어서 'g', 'l', 'e' 각 데이터를 패킷으로 만들어 보낼 것입니다. 수신호스트로부터의 ACK가 언제 오는가는 전혀 관계가 없고언제 오든지 오기만 하면 되는 것입니다.

 

2. Nagle 알고리즘

- 네트웍에서 Nagle 알고리즘은 "가능하면 조금씩 여러 번 보내지 말고 한번에 많이 보내라(Effective TCP)" 라는 원칙을 기반으로 만들어진 알고리즘입니다.

- Nagle 알고리즘의 원리는 ACK를 받은 다음에 데이터를 보내고 ACK를 받을 때까지 출력버퍼의 데이터를 저장하였다가 ACK를 받으면 버퍼의 데이터를 모두 패킷으로 만들어 보낸다는 것입니다. 예를 들어 A 'N'이라는 데이터를 패킷으로 만들어 보내고, 계속해서 다음 데이터를 보내는 것이 아니라 출력버퍼로 보내어 저장시켜 둡니다. 그러다가 ACK가 오면 출력버퍼에 저장된 'agle'라는 데이터를 보냅니다.


- TCP 소켓은 Default Nagle 알고리즘을 적용하고 있습니다.

 

3. Nagle 알고리즘의 장단점

  - 장점네트워크의 효율성이 높아짐. (똑같은 데이터를 보내더라도 생산하는 패킷이 적음)

  - 단점 : 송신 호스트가 ACK를 받을 때까지 기다려야 하므로 전송 속도가 느려짐

 

4. Nagle 알고리즘의 중단

- 몇몇 네트웍 관련 프로그램에서는 네트웍의 전송량이나 부하보다는 빠른 응답속도를 더 중요시 여기는 상황이 있습니다. 그러한 때에는 TCP_NODELAY  라는 옵션을 사용하여 Nagle 알고리즘을 제거 할 수 있습니다.

 

- TCP_NODELAY 옵션이

     1(TRUE) : Nagle 알고리즘을 적용하지 않습니다.

     2(FALSE): Nagle 알고리즘을 적용합니다.

 

int opt_val = TRUE;

setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt_val, sizeof(opt_val));

 

해당 옵션의 사용은 네트웍 부하를 극대화 시켜주면서 서버의 전체적인 성능을 무척 감소하기때문에 꼭 필요한 경우에만 매우 주의를 해서 사용해야 합니다.

- 전송은 작은 단위로 자주 이루어지지만 즉각적인 응답은 필요 없는 어플리케이션에서만 사용 되어야 합니다.(마우스 움직임 같은)

 

- Nagle 알고리즘은 리얼타임시스템에서의 제어와 특히나 인터렉티브한 키 입력을 하는 어플리케이션에서는 안 좋은 영향을 미칩니다. 선택적으로 Nagle 알고리즘을 통과하는 한가지 방법은 Out-of-bind 메시지 시스템을 쓰는 것입니다. 그러나 이것은 내용물에 제약이 있고 또 다른 문제(순서의 상실: loss of sequentiality)를 일으킬 수 있습니다

출처 : http://depiness.egloos.com/772710

 

HTTP Session Hijacking(혹은 Session ID Hijacking)이라는 공격 기법은 웹 브라우징시 세션 관리를 위해 사용되는 Session ID를 스니핑이나 무작위 추측 공격(brute-force guessing)을 통해서 도용하는 기법이다. 먼저 이러한 공격에 대한 기초적인 배경지식으로 HTTP 프로토콜의 특성 및 Session ID에 대해 이해해보도록 하겠다.

HTTP 프로토콜의 특성
HTTP는 기본적으로 비연결유지(stateless) 프로토콜이다. 반면, telnet과 ftp와 같은 프로토콜은 클라이언트와 서버 사이에 하나의 연결(session)이 성립되어 통신하는 프로토콜이다. 따라서, 우리가 보통 웹 브라우저를 열어 URL을 입력하고 해당 홈페이지에 들어간다는 것은 해당 홈페이지에 포함되어 있는 페이지(html), 그림(jpg, gif 등), 자바스크립트(js) 등을 다운받기 위해 개별적인 여러 개의 80 요청(request)을 발송한 후 서버로부터 각각의 응답(reply)을 받는 것을 의미한다.

이러한 일련의 요청과 응답이 이루어진 후 해당 서버와의 통신은 다시 종료된다. 위와 같은 기본적인 지식을 알고 있다면 다음과 같은 질문을 할 수 있다. HTTP는 비연결유지 프로토콜이라고 하였는데 Session Hijacking 이란 공격은 어떻게 가능한 것인가? 이는 HTTP 세션 관리를 위해 사용되는 Session ID를 통해서 가능하다.

Session ID란 무엇인가?
웹 서버는 다수의 웹 페이지 요청자를 구별하기 위하여 각각의 사용자의 세션에 대해서 임의의 긴 문자열 값인 Session ID를 부여한다. 사용자가 홈페이지 방문시 혹은 인증 로그인시에 생성된다. 이러한 Session ID는 사용자의 계정, 암호, 그 밖의 IP 주소, timestamp 등의 여러 파라미터들을 조합하여 생성할 수 있다.


Session ID는 사용자와 일련의 웹 서핑 동작을 연결시켜줌으로써 웹 사이트 로그인 후 다른 페이지 방문시마다 매번 로그인을 하지 않아도 되는 편리함을 제공해준다.

우리가 신문 홈페이지나 포털 사이트에 들어갈 때 광고 배너가 자동으로 바뀐다던지, 쇼핑몰이나 인터넷 서적몰에서 구매 카트의 목록이 유지되는 것은 모두 이러한 원리이다.

즉, Session ID를 통해 인증과 인가(authentication & authorization)라는 세션 관리를 수행할 수 있다.


Session ID는 어디에 존재하는가?
Session ID는 우리가 흔히 듣는 쿠키(cookie)라는 곳에 저장되는 것이 일반적이다. 그러나 가끔은 웹 브라우저 주소창 URL이나 HTML 페이지 폼 소스 상의 hidden 필드에 포함되어 드러나기도 한다.

1)쿠키


2)웹 브라우저 주소창의 URL


3)웹 페이지 폼 소스 상의 hidden field



Session ID의 취약성은 무엇인가?
웹 서버에서의 Session ID 생성 기법 및 관리 기법에 따라서 다음과 같은 취약점이 존재할 수 있다.

  • 강력하지 못한 알고리즘(Weak Algorithm): Session ID 스트링 값을 생성함에 있어서 공격자가 reverse 엔지니어링이 가능한 쉬운 알고리즘으로 생성될 경우 cracking이나 brute-force guessing 공격의 위험이 있다.
  • 길이가 짧은 Session ID: 강력한 암호 알고리즘을 사용하더라도 그 길이가 충분하지 않고 짧은 경우에는 cracking이나 brute-force guessing 공격의 위험이 있다.
  • 계정 잠금 기능 미비: 로그인 패스워드의 특정 회수 실패에 대해서는 보통 계정잠금 기능이나 해당 IP 차단 기능을 구현하고 있습니다. 그러나 보통 Session ID에 대한 무결성 침해나 특성 회수 실패에 대해서는 이러한 잠금 기능 구현이 미비하다. 따라서, brute-force guessing 공격의 위험이 있다.
  • 무한 만료의 Session ID: 사용자의 로그 아웃 이후에도 서버측에서 해당 세션 ID값을 폐기하지 않고 무한정 유효 인정한다면 cookie sniffing이나 프락시 서버의 로그 취득을 통하여 session ID 공격이 가능하다.
  • 평문으로 전달되는 Session ID: 서버에서 클라이언트로의 session ID 쿠키 전달 방식이 비암호화 방식일 경우에는 sniffing을 통하여 해당 값이 노출되어 공격 받을 수 있다. 특히 Session ID 값 자체가 사용자명이나 암호 등의 평문으로 구성되어 있는 경우에는 직접적인 공격이 가능하다.

    위와 같은 취약성에 대한 Session ID 공격의 유형은 다음과 같다.

    Session ID 공격유형
  • 직접적인 Cookie Sniffing을 통한 Session ID 도용
  • 간접 우회 공격을 통한 Session ID 도용
  • Brute-force guessing을 통한 Session ID 도용

    지금까지 Session ID가 무엇인지, 어떤 형태로 존재하는지, 왜 취약한지에 대해서 알아보았다. 다음에는 실제 공격 유형에 대해 살펴보고, 대응 방안에 대해서도 논의해 보도록 하겠다.

    지난 회에서 기본적으로 Session ID가 무엇인지, 어떤 형태로 존재하는지, 왜 취약한지에 대해서 설명했다. 이번 회에는 취약한 Session ID에 대한 공격이 어떤 방식으로 일어나는지 살펴보고, 대응 방안에 대해서 살펴보도록 하겠다.

    Session ID 공격 유형
    Session ID에 대한 공격은 크게 2 단계로 이루어진다. 1단계는 Session ID 취득 단계이고 2단계는 취득한 Session ID 공략 단계이다.




    [그림1]HTTP Session Hijacking 공격 흐름도

    공격자는 Session ID를 취득하기 위하여 웹 서버와 웹 클라이언트의 트래픽을 직접적으로 스니핑하거나, 웹 서버 상에 공격 코드를 삽입하여 두고 사용자가 클릭할 때 Cookie, Session ID 값을 전송받을 수 있도록 한다. 웹 서버와 웹 클라이언트 사이의 트래픽을 직접적으로 스니핑하는 방법은 일반적인 네트워크 트래픽 스니핑 기법을 통해 가능하다.

    POST /xxxxxx HTTP/1.1Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-shockwave-flash, */*Referer: http://xxx.xxx..comAccept-Language: koContent-Type: application/x-www-form-urlencodedAccept-Encoding: gzip, deflateUser-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)Host: ibn.kbstar.comContent-Length: 297Connection: Keep-AliveCache-Control: no-cacheCookie: Session=QJ48621878865
    [그림2]네트워크 트래픽 스니핑을 통한 HTTP 요청 헤더 안의 쿠키값

    웹 서버 상의 HTML 코드 삽입이 가능한 페이지는 주로 사용자가 글을 게시할 수 있는 게시판이나 자료실 등에 존재한다. 정상적인 글을 게재하는 대신 공격자는 HTML 코드 및 스크립트를 심어 넣는다. 일반 사용자는 해당 게시물을 열람하게 될 때 자신도 모르는 사이 Cookie, Session ID 정보가 제 3 의 공격자 서버나 이메일로 전송되게 된다. (이러한 공격 기법을 Cross-Site Scripting이라고 부른다.)

    <form name=f method=POST action="http//host/hello.php"><input type=hidden name="name"value="<script>alert(document.cookie)</script>"></form><script>f.submit()</script>
    [그림3]쿠키값 탈취를 위한 HTML 삽입 코드 예

    결과적으로 공격자는 취득한 타인의 Session ID 값을 웹 서버에 요청함으로써 HTTP Session Hijacking 공격을 시도할 수 있다. 물론 이 공격이 성공하려면 Session ID 값이 유효해야 하므로, 사용자가 로그온 한 상태에서 공격이 이루어져야 하거나 Session ID 값의 유지 시간이 긴 경우라는 제한 사항이 필요하다.

    그러나, 기본적으로 잘못 설계된 세션 관리 기법을 사용하고 있는 웹 서버는 이러한 Hijacking 공격에 취약할 수 밖에 없다. 굳이 타인의 Session ID 값을 직.간접적으로 취득하지 않고도 무작위 추측 대입(Brute-force Guessing)함으로써 공격이 가능하다. 공격자는 정상적인 로그인 과정시 분석한 자신의 Cookie 값, 웹 브라우저의 주소창의 URL, 웹 페이지 폼 소스 hidden field 내에 노출된 Session ID 값 자체를 분석한다.

    Session ID 생성 방식의 취약점을 파악한 후, 공격자의 컴퓨터에서 로컬 프록시 툴이나 웹 브라우저 창 URL 주소창에서 직접 Sessioion ID 값 단일 대입을 시도한다. 더 나아가 자동적인 Session ID 대입 스크립트를 작성하여 공격할 수도 있다.

    http://mmail.xxx.co.kr/mletter1/read_mail.asp?id=2266&tableName=musicMail1927&fromEmail=xxx@xxx.co.kr
    http://www.xxx.co.kr/view/UID2305670410000
    http://www.xxx.co.kr/view/UID2305670410341
    http://www.xxx.co.kr/view/UID2305670411302
    [그림4] 정상적인 Session ID 값


    http://mmail.xxx.co.kr/mletter1/read_mail.asp?id=3734&tableName=musicMail1927&fromEmail=xxx@xxx.co.kr
    http://www.xxx.co.kr/view/UID2305670410001
    http://www.xxx.co.kr/view/UID2305670410002
    http://www.xxx.co.kr/view/UID2305670419999
    [그림5] 무작위 추측 대입된 Session ID 값

    대응 방안
    HTTP Session Hijacking 공격에 대하여 웹 어플리케이션 개발자는 다음과 같은 점을 고려하여 세션 관리 기법을 구현하여야 한다.

    1.Session ID 생성 범위값을 사용자 수에 대비하여 충분히 큰 값으로 설정한다.
    2.Session ID는 가능한 한 추측 불가능(random)하게 생성한다. 무작위 추측 대입 공격을 할 때 공격자는 그만큼 더 많은 시간을 투입하여야 하며 현재 연결되어 활성화 되어 있는 유효한 Session ID 값을 찾는 확률은 낮아진다.
    3.Session Timeout 기능과 Session ID 재생성 기능을 사용한다. 일정 시간 동안 활동이 없는 사용자는 새로운 Session ID로 재로그인하도록 하고, 사용자 로그 아웃 시에는 Session ID 값을 폐기한다. 장시간 접속이 필요한 어플리케이션의 경우에는 일정 주기마다 Session ID값을 자동으로 재생성하여 세션을 유지하도록 한다.
    4.무작위 추측 대입(Brute-force Guessing)에 대비하여 일정 회수 이상의 인증 실패시에는 사용자 잠금 기능을 구현한다.
    5.로그인 이후에도 중요한 서비스 이용시에는 사용자 인증을 통하여 인가된 사용자만이 해당 서비스를 이용할 수 있도록 통제한다.
    6.Cookie 내용 안의 Session ID와 기타 변수값 자체를 암호화한다.
    7.웹 서비스 자체의 중요성에 따라 Cookie가 전달되는 방식을 SSL로 구현함으로써 스니핑 공격에 대응할 수도 있다.
    8.웹 서비스 상에 공격자가 HTML 공격 코드 삽입이 가능한 페이지가 있는지 점검한다. 직접적인 공격 코드 삽입을 차단할 수 있도록 특수 문자 및 스크립트 코드를 필터링하여야 한다.

    지금까지 HTTP Session Hijacking 공격 기법에 대해서 알아보았다. 대부분이라고 말할 수는 없지만 요즘 인터넷에 서비스되고 있는 인기 있는 웹 컨텐츠 서버는 기본적으로 안전한 세션 관리 기법으로 구현되어 있다. 그러나, 웹 컨텐츠의 인지도와는 별도로 이러한 공격 기법에 대한 인지 없이 비보안적으로 구현된 e-메일 카드 서비스, 전자 앨범 서비스 사이트 등이 다수 존재하는 것 또한 현실이다.

    HTTP Session Hijacking공격에 대한 대비는 안전한 웹 어플리케이션 구축을 위한 여러가지 항목 중에 필수적인 한 가지 사항이며, 기획 및 개발구현 단계에서 반드시 고려되어야 한다.

    출처: 코코넛 시큐레터


  • http://dnstunnel.de/

    http://www.splitbrain.org/blog/2008-11/02-dns_tunneling_made_simple

    http://psichron.za.net/downloads/dns_tunneling.txt


    나 공부할 것 임 영어라 힘들겠구만


    관련 자료 :
    http://pdos.csail.mit.edu/papers/p2pnat.pdf

    http://www.serious-code.net/moin.cgi/Scrap_2fNatAndP2pApplications

    http://reports-archive.adm.cs.cmu.edu/anon/isri2005/CMU-ISRI-05-104.pdf

    http://lshsblog.blogspot.com/2011/04/hole-punching_18.html   // 홀펀칭 유명 논문 번역

    http://elky.tistory.com/259


    멀티스레드 윈속 서버 프로그램의 문제점은 사용자의 수가 많아지면 스레드의 동적 생성과 스레드간의 잦은 컨텍스트 스위칭으로 인한 오버헤드가 크다는 점이었다. 이러한 점을 극복하기 위해 도입된 것이 바로 IOCP이다. 방금 설명한 것처럼 이는 멀티스레드 프로그래밍에서 유용하게 사용할 수 있으며 그 중에서도 소켓이나 파일, 메일슬롯, 파이프와 같은 입출력 관련 프로그램에서 유용하게 사용할 수 있다.


    필자는 IOCP를 파일 I/O가 많은 응용프로그램과 네트웍 I/O가 많은 윈속 프로그램에서 사용해봤는데 그냥 단순한 멀티스레드 프로그램을 작성하는 것보다 괜찮은 성능을 가짐을 알 수 있었다. 부하가 그리 크지 않다면 IOCP를 사용하나 사용하지 않으나 성능상에 큰 차이가 없다. 하지만 부하가 클 경우에는 (예를 들어 윈속 서버 프로그램이라면 현재 접속 사용자수가 많을 경우에는) 상당한 차이를 가져온다는 점을 잘 새겨두기 바란다. 하지만 파일 I/O가 아주 빈번한 응용프로그램에서는 IOCP를 사용한 멀티스레드 프로그램이나 그냥 멀티스레드 프로그램이나 성능에 있어 별 차이가 없다. 그 이유는 스레드로 인한 오버헤드보다 파일 I/O 자체로 인한 오버헤드가 더 크기 때문이었다.


    단, IOCP가 무슨 마법처럼 시스템이 가진 하드웨어 제약조건 이상으로 많은 수의 사용자를 처리할 수 있도록 해주거나 하는 것은 아니란 점을 명심하기 바란다. 부하가 많은 시점에 그냥 멀티스레드 프로그래밍을 하는 것보다 더 좋은 성능을 보일 뿐이다. 획기적으로 좋은 성능을 보이거나 하는 마술과 같은 것은 아니란 것이다. 또한 IOCP는 NT 4.0, 2000, XP에서만 사용가능하다는 점도 알아두기 바란다.


    먼저 IOCP라는 것이 그리 이해하기 쉬운 편은 아니고 이해해서 사용하기는 더욱 어렵다는 점을 밝히고 싶다. 겁먹으라고 하는 소리는 아니고 잘 이해가 안되어도 필자 탓을 하거나 자신의 머리탓(?)을 하지말고 한번 더 읽어보라는 의미에서 하는 말이다. 참고문헌 2>와 3>에 필자가 처음 IOCP를 공부할 때 봤던 책과 인터넷 기사를 적어두었다. 참고하기 바란다. 또, 마이크로소프트 플랫폼 SDK의 예제 프로그램 중에 보면 윈속에서 IOCP를 어떻게 사용할 수 있는지 보여주는 간단한 예가 있다. 참고문헌 4에 적었다. 사실 이번 연재에서 작성한 서버 예제 프로그램도 이 것을 바탕으로 작성되었다. 클라이언트 예제 프로그램은 사실 지난 회와 동일하다. 그렇기 때문에 클라이언트 프로그램에 대해서는 다루지 않겠다.


    1. IOCP의 필요성 ?

    IOCP가 왜 필요한지 알아보려면 기존 멀티스레드 프로그래밍의 제한점을 먼저 이해해야 한다.


    많은 수의 스레드 생성으로 인한 오버헤드 : 확장성의 제한


    동시에 여러 사용자를 처리할 수 없는 프로그램을 서버 프로그램이라고 부를 수 없을 것이다. 서버 프로그램이 되려면 동시에 여러 사용자로부터의 요구를 처리할 수 있어야 하고 그렇게 하기 위해서 스레드를 사용할 수 밖에 없다. 결론적으로 진정한 다중 사용자용 서버 프로그램을 짜본 사람이라면 동시 사용자 처리를 위해 누구나 스레드를 사용하고 있을 것이다. 대부분의 경우 지난 회에 살펴본 예제 프로그램처럼 현재 접속 중인 사용자의 수만큼 스레드를 만드는 방식을 취하게 된다. 즉 사용자마다 그 요구를 처리하기 위한 전담 스레드를 생성하는 것이다.


    하지만 이 방식의 문제점 중의 하나는 바로 현재 접속 중인 사용자의 수가 늘어날 경우에 발생한다. 스레드의 생성은 당연히 자원의 사용을 가져온다. 어느 수 이상으로 스레드가 생성되면 프로그램의 성능이 오히려 전체적으로 저하된다. 이유는 너무 많은 스레드가 생성되면 아무래도 그로 인해 자원이 많이 필요하게 되고 또 그 많은 스레드들간의 컨텍스트 스위칭으로 인해 실제 CPU가 어떤 일을 하는 시간보다 컨텍스트 스위칭하는데 상당한 시간을 보내게 되기 때문이다. 예를 들어 CPU의 수보다 스레드의 수가 많다면 사실 스레드간의 컨텍스트 스위칭으로 인한 오버헤드가 있다고 볼 수 있다. (사실 CPU의 수만큼 스레드의 수가 존재하는 것이 이상적이지만 이는 사실상 불가능한 일이다. 이는 만들고자 하는 응용프로그램의 특성에 따라 굉장히 달라질 수 있다.)


    IOCP는 이러한 단점을 극복하기 위해 하나의 스레드가 하나 이상의 사용자로부터의 요구를 처리할 수 있도록 해준다. 그렇다고 하나의 스레드만을 생성하는 것은 아니다. 여러 개의 스레드를 생성하지만 한 스레드가 한 사용자만을 전담하는 방식은 아니라는 것이다. 즉, 실행되는 스레드의 수를 적게 해서 이로 인한 컨텍스트 스위칭의 수를 줄이는 것이다. 이것이 가능하려면 이제 뒤에서 살펴볼 것처럼 프로그램내에서 I/O시에 비동기 방식을 사용해야 한다.


    비동기 I/O는 서버 프로그래밍의 필수


    서버 프로그램에서 성능 향상을 위해서 사용할 수 있는 다른 하나의 테크닉은 비동기(Asynchronous) I/O를 사용하는 것이다. 이를 이용하면 동시에 여러 개의 I/O 작업을 수행할 수 있는데 이는 어디까지나 작업의 시작만 비동기로 가능하다는 것이지 작업이 끝나는 부분은 즉, I/O 결과를 받는 부분은 동기화가 되어야 한다는 것이다. 만일 비동기 I/O의 결과를 그냥 무시해도 좋은 프로그램이라면 또다른 이야기가 되겠지만 아마 대부분의 프로그램에서는 비동기 I/O를 수행하고 그 결과를 살펴봐야 할 것이다.


    비동기 I/O에는 여러가지 방식이 존재한다. 간략히 참고 1에 윈도우에서 지원되는 비동기 I/O 방식을 나열해 보았다. 당연한 이야기이지만 이러한 비동기 I/O 방식은 특히 시간이 오래 걸리는 작업을 할 때 적합한 방식이다. 이러한 비동기 I/O 방식은 IOCP와 결합되었을 때 최적의 성능과 확장성을 자랑한다. 다시 정리해서 말하자면 비동기 I/O의 성능은 I/O가 끝났을 때 그 결과를 어떻게 확인하느냐에 달려 있는데 IOCP는 이러한 비동기 I/O를 가장 효율적으로 사용할 수 있게 해준다.


    참고 1. 윈도우의 비동기 I/O


    윈도우에서는 다양한 방식의 비동기 I/O를 제공한다 (사실 너무 다양한 방법을 제공한다.) 여기서는 간략히 언급하기로 하겠다. 다음에 기회가 닿으면 파일 I/O 관련 연재 기사를 다뤄볼 생각인데 그 때 자세히 언급하기로 하겠다.


    1> 오버랩드 I/O를 사용하기.


    예로 파일 I/O를 들어보자. 파일을 오픈할 때 CreateFile API를 사용하는데 이 때FILE_FLAG_OVERLAPPED를 인자로 주면 오버랩드 I/O를 수행할 수 있다. ReadFile과 WriteFile을 사용하여 I/O를 수행하게 되는데 이 함수들은 실행이 끝날 때까지 기다리지 않고 바로 리턴한다(비동기 I/O니까 당연한 이야기이지만). 이 때 마지막 인자로 OVERLAPPED 구조체를 사용하는데 여기에 이벤트(지난 회에 설명한 바 있다)를 지정하도록 되어있다. 작업이 끝나면 이 이벤트로 시그널이 가게 된다. 이벤트를 사용하는 대신에 함수의 실행이 끝났는지를 검사하기 위해 GetOverlappedResult 함수를 호출할 수도 있다. 참고로 ReadFile이나 WriteFile과 같은 함수는 꼭 파일 I/O에 사용되는 것이 아니란 점도 알아두기 바란다. 소켓에서 데이터를 읽고 쓰는데도 사용할 수 있다.


    2> 콜백 함수 사용하기


    기본적으로는 1<의 방식과 갖다. 다만 이벤트를 사용하는 대신에 콜백 함수를 지정해서 작업이 끝나면 그 함수를 호출하도록 하는 것이다. 이때는 ReadFile, WriteFile과 같은 함수 대신에 ReadFileEx와 WriteFileEx와 같은 함수를 사용해야 한다. 이 함수들은 인자 중의 하나로 콜백 함수의 주소를 받아들이도록 되어있다.

    3> IOCP 사용하기

    사실 IOCP를 비동기 I/O 작업 방식이라고도 할 수 있는데 이에 대해서는 이 기사의 뒷부분에서 자세히 살펴볼 것이다.


    --------------------------------------------------------------------------------

    지금까지 살펴본 것과 같은 기존의 멀티스레드 서버 프로그래밍의 문제점을 해결하기 위해 만들어진 것이 바로 IOCP이다. 기본적으로 IOCP는 비동기 I/O 작업을 지원하면서 적은 수의 스레드로 최대한의 요청을 처리하기 위한 방법이란 점이라고 이해하면 된다. 너무 많은 스레드가 동시에 동작함으로 인한 문제를 해결하면서 비동기 I/O 작업시 결과를 체크해야 하는 문제를 해결함으로써 서버 프로그램의 성능을 극대화하는 것이다.


    2. IOCP란 ?


    IOCP란 특정 입출력 객체(예를 들면 파일, 소켓, 메일 슬롯 등등)와 관련된 일종의 I/O 작업 결과 큐라고 생각할 수 있다. 좀더 자세히 설명하자면 먼저 IOCP 객체가 별도로 생성되어야 한다. 그 다음에 이 객체와 입출력 객체 중의 하나가 연결되어야 한다. 다음으로 이 입출력 객체에 비동기 I/O 작업이 수행되면 운영체제가 이 큐에 그 비동기I/O의 결과를 집어넣게 된다.


    또한 이 큐는 하나 이상의 스레드와 연관지어지게 된다 (스레드의 수는 비동기 I/O의 특성에 따라 달라지게 된다. 만일 I/O가 오래 걸리는 것이라면 스레드의 수는 적어도 관계없다. 하지만 I/O가 시간이 아주 조금밖에 안 걸리는 것이라면 스레드의 수는 많아야 한다). 운영체제는 큐에 결과가 있고 관련 스레드들 중에서 놀고 있는 놈이 있으면 그 스레드가 결과를 받아서 다음 작업을 수행할 수 있게 해준다. 즉, IOCP와 관련되어 동작할 수 있는 스레드를 미리 여러 개 만들어 놓고 이 중에서 필요에 따라 놀고 있는 것을 가져다 큐에서 비동기 I/O 결과를 읽어가도록 하는 것이다. 참고로 한 IOCP 객체는 동시에 여러 입출력 객체와 연관지어질 수 있다.


    자 이러한 과정을 코드를 통해 좀더 자세히 살펴보자. 그림 1을 참고하기 바란다. 본 기사의 서버 예제 프로그램의 코드를 바탕으로 설명하겠다.


    < 그림 1. IOCP의 동작 >


    1> IOCP의 생성


    먼저 첫번째 절차는 IOCP를 생성하는 것이다. 이는 CreateIoCompletionPort 함수를 통해 가능하다. 이 같은 함수를 이용해 입출력 객체와 IOCP를 연관짓는데 사용할 수 있다. 다음은 IOCP를 일단 생성하는 예(CreateIoCompletionPort 함수의 첫번째 인자로 INVALID_HANDLE_VALUE를 지정해야 한다. 이 함수에 대한 보다 상세한 설명은 참고 2를 보기 바란다)이다. 생성의 결과는 HANDLE로 리턴된다.

    g_hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

    if (NULL == g_hIOCP)

    {

    printf("CreateIoCompletionPort failed: %dn", GetLastError());

    CleanUp();

    }


    2> IOCP 큐에서 결과를 읽어들일 스레드 생성


    앞서 이야기한 것처럼 IOCP와 연관된 입출력 객체에 비동기 I/O를 수행하면 그 결과가 IOCP 큐에 쌓인다고 하였다. 이 큐에서 결과를 읽어들이는 일을 수행하는 스레드를 만들어야 한다. 다른 스레드 생성과 특별히 다를 것은 없다. 단 생성할 스레드의 수는 CPU수 X 2로 되어있다. 이는 마이크로소프트에서 권장하는 방식이다. 응용프로그램에 따라 이것이 적당할 수도 있고 훨씬 더 많은 스레드가 필요할 수도 있다. 이를 위해서 GetSystemInfo라는 함수를 이용해서 현재 시스템의 CPU수를 알아내는 코드가 들어있다.


    #define MAX_WORKER_THREAD 16


    DWORD g_dwThreadCount;

    unsigned int g_hThreads[MAX_NUMBER_OF_THREADS];


    SYSTEM_INFO systemInfo;

    DWORD dwThreadId;


    GetSystemInfo(&systemInfo);

    g_dwThreadCount = systemInfo.dwNumberOfProcessors * 2;

    for (DWORD dwThread = 0;dwThread < g_dwThreadCount; dwThread++)

    {

    g_hThreads[dwThread] = _beginthreadex(NULL, 0, EchoThread,

    g_hIOCP, 0, &dwThreadId);

    If (g_hThreads[dwThread] == NULL)

    {

    printf(“%d번째 스레드 생성에 실패했습니다.n”, dwThread);

    }

    }


    위에서 볼 수 있는 것처럼 스레드의 생성에는 _beginthreadex 함수를 사용하였다. 스레드 함수는 EchoThread이며 스레드 함수의 인자로는 IOCP 핸들을 넘긴다. EchoThread 함수의 자세한 내용은 5>에서 살펴볼 것이다.


    3> IOCP와 입출력 객체의 연결


    다음은 이 IOCP와 입출력 객체를 연결하는 부분이다. 입출력 객체는 반드시 비동기 I/O 모드로 오픈되어야 한다. 연결된 객체에 대한 비동기 오버랩드 I/O 결과가 이 IOCP 큐에 들어간다. 예를 들어 소켓과 IOCP를 연결하는 간단한 예를 보면 다음과 같다.


    SOCKET sh;


    sh = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL,

    0, WSA_FLAG_OVERLAPPED);…

    if (sh != INVALID_SOCKET)

    {

    CreateIoCompletionPort((HANDLE)sh, g_hIOCP, (ULONG_PTR)0, 0);


    먼저 소켓을 생성할 때 socket 함수를 사용한 것이 아니라 WSASocket 함수를 사용하였고 마지막 인자로 WSA_FLAG_OVERLAPPED가 지정되었다. 그 다음에 앞에서와 같은 CreateIoCompletionPort 함수를 사용하고 그 첫번째 인자로 소켓의 값을 지정하면 된다. 즉, CreateIoCompletionPort 함수는 IOCP의 생성에도 사용되고 생성된 IOCP와 입출력 객체를 연결하는데도 사용된다. 참고 2에서 CreateIoCompletionPort 함수의 세 번째 인자 설명을 보면 알 수 있겠지만 세 번째 인자가 가장 중요한 역할을 한다.


    앞서 잠깐 언급했던 것처럼 IOCP는 여러 개의 입출력객체와 동시에 연관지어질 수 있다. 예를 들어 여러 개의 소켓이
    하나의 IOCP와 연관될 수 있다. 즉 그러한 소켓들에 대해 이루어지는 비동기 작업의 결과는 모두 그 하나의 IOCP 큐로 들어간다는 것이다. 그러기 때문에 IOCP 큐에서 비동기 작업 결과를 읽어들일 때 이 결과가 도대체 어느 입출력 객체로부터 온 것인지를 구분할 수 있는 방법이 있어야 한다. 이 때 CreateIoCompletionPort 함수의 세 번째 인자로 지정했던 값이 구분하는 역할을 담당한다. 뒤에서 살펴보겠지만 IOCP 큐에서 결과를 읽어들일 때 사용하는 함수는GetQueuedCompletionStatus라는 것이다. 이 함수의 세 번째 인자로 앞서 CreateIoCompletionPort 함수에서 지정했던 값이 넘어오게 되어 있다. 예제 프로그램에서는 입출력 객체마다 다음과 같은 구조체를 생성하고 이를 IOCP 객체와 연관지을 때 세번째 인자로 지정할 것이다.


    // IOCP와 연관되는 소켓마다 할당되는 구조체

    typedef struct _PER_SOCKET_CONTEXT

    {

    SOCKET Socket;

    PPER_IO_CONTEXT pIOContext;

    } PER_SOCKET_CONTEXT, *PPER_SOCKET_CONTEXT;


    위에서 Socket은 클라이언트가 하나 연결될 때마다 부여되는 소켓이다. pIOContext는 이 소켓과의 입출력 작업에 사용되는 메모리 버퍼와 각종 구조체를 모아둔 구조체로 이 소켓내에서 벌어지는 입출력 작업의 상태를 나타낸다고 생각하면 된다. 다음과 같이 정의되어 있다.


    #define MAX_BUFF_SIZE 8192

    // 소켓에 대한 입출력 작업에 사용되는 구조체

    typedef struct _PER_IO_CONTEXT

    {

    WSAOVERLAPPED Overlapped;

    char Buffer[MAX_BUFF_SIZE];

    WSABUF wsabuf;

    int nTotalBytes;

    int nSentBytes;

    IO_OPERATION IOOperation;

    } PER_IO_CONTEXT, *PPER_IO_CONTEXT;


    먼저 첫번째 필드인 Overlapped는 사실 ReadFile, WriteFile과 같은 함수를 이용해서 수행하는 비동기 I/O에서 사용하는 OVERLAPPED 구조체와 동일한 것이다. typedef로 이름만 바꾸었을 뿐이다. 뒤에서 WSARecv와 WSASend를 이용해서 비동기 I/O를 해볼 텐데 그 때 이 필드가 사용된다. 그 함수들을 호출할 때 로컬 변수로 사용하면 안 될까 생각할 수도 있는데 이 변수는 작업이 끝날 때까지 접근이 가능해야 하기 때문에 이렇게 글로발하게 별도로 잡아두는 것이다. (로컬 변수로 잡고 그걸 인자로 비동기 함수를 호출하면 그 변수가 선언한 블럭을 벗어날 경우 그 로컬 변수는 더 이상 유효하지 않다. 이런 문제를 해결하기 위함이다)


    사실 이 구조체는 의도적으로 WSAOVERLAPPED 타입의 필드로부터 시작한다. 비동기 I/O 작업에 사용되는 WSASend, WSARecv함수의 경우 인자 중에 WSAOVERLAPPED 타입의 변수를 받아들이는 인자가 있다. 또한 비동기 I/O가 끝나고 그 결과를 IOCP 큐에서 읽어들일 때 앞서 사용했던WSAOVERLAPPED 타입의 변수를 그대로 받아볼 수 있다.


    사실 Overlapped가 이 구조체의 첫 번째 필드이기 때문에 이 필드의 주소나 이 구조체의 주소나 동일하다. WSASend와 WSARecv를 이용해 비동기 I/O를 개시할때 이 구조체의 Overlapped 필드의 주소를 넘기면 사실 이것이PER_IO_CONTEXT 타입 변수의 주소를 넘긴 것이나 다름없다. 그렇게 해서 비동기 I/O의 결과를 큐에서 꺼낼 때 현재 작업의 상태를 알 수 있는 것이다. 앞서 이야기한 것처럼 PER_IO_CONTEXT 구조체는 현재 비동기 I/O 작업의 상태를 나타낸다.


    두 번째 필드인 Buffer는 읽기/쓰기 작업을 할때 사용할 메모리 영역이다. 세 번째 필드인 wsabuf는 읽기/쓰기 작업시 데이터의 시작 포인터와 데이터 크기를 지정하는데 사용되는 구조체이다. WSASend와 WSARecv 함수의 인자로 필요하다. 네 번째 인자인 nTotalBytes는 쓰기 작업시 전송해야할 데이터의 양을 나타낸다. 다섯 번째 인자인 nSendBytes는 지금까지 전송된 데이터의 양을 나타낸다. 마지막 인자인 IOOperation은 다음과 같이 정의된 열거자로서 현재 소켓에 대해 진행 중인 작업의 종류를 나타낸다.


    typedef enum _IO_OPERATION

    {

    ClientIoRead, // 읽기 작업 진행 중

    ClientIoWrite // 쓰기 작업 진행 중

    } IO_OPERATION, *PIO_OPERATION;


    이제 이를 바탕으로 예제 프로그램의 코드를 살펴보자. 다음에서 볼 수 있는 것처럼 클라이언트로부터의 요청이 들어오기를 대기하다가 요청이 들어오면 그로 인해 생성되는 소켓을 인자로 위의 작업을 수행하는UpdateCompletionPort라는 함수를 별도로 만들었다.


    SOCKET sdAccept = INVALID_SOCKET;

    PPER_SOCKET_CONTEXT lpPerSocketContext = NULL;


    while (g_bEndServer == FALSE)

    {

    // 클라이언트가 들어오기를 대기한다.

    sdAccept = WSAAccept(g_sdListen, NULL, NULL, NULL, 0);

    if (SOCKET_ERROR == sdAccept)

    {

    printf("WSAAccept: %dn", WSAGetLastError());

    CleanUp();

    }

    printf("클라이언트가 하나 들어왔습니다n.");

    // 만들어진 sdAccept 소켓에 앞서본 PER_SOCKET_CONTEXT 구조체를 할당한다.

    // 그리고나서 이를 IOCP 객체와 연결한다. 두 번째 인자로는 이제 일어날 작업의

    // 종류를 명시한다. 에코우 서버이므로 첫 번째 할 작업은 클라이언트로부터

    // 데이터를 읽는 것이기 때문에 ClientIoRead를 명시한다.

    lpPerSocketContext = UpdateCompletionPort(sdAccept, ClientIoRead, TRUE);

    if (NULL == lpPerSocketContext)

    {

    CleanUp();

    }

    // …


    UpdateCompletionPort 함수의 내용은 다음과 같다. 첫 번째 인자로 지정된 소켓을 바탕으로 앞서본 PER_SOCKET_CONTEXT 구조체를 할당한다. 이것과 소켓을IOCP 객체와 연결한다. 두 번째 인자로는 이제 이 소켓에 일어날 작업의 종류를 명시한다. 에코우 서버이므로 첫 번째 할 작업은 클라이언트로부터 데이터를 읽는 것이기 때문에 ClientIoRead를 명시한다.


    // 첫번째 인자로 명시된 소켓을 IOCP에 연결짓는다.

    PPER_SOCKET_CONTEXT UpdateCompletionPort(SOCKET sd, IO_OPERATION ClientIo)

    {

    PPER_SOCKET_CONTEXT lpPerSocketContext;


    // PER_SOCKET_CONTEXT를 할당하는데 CtxtAllocate 함수를 사용한다.

    lpPerSocketContext = CtxtAllocate(sd, ClientIo);

    if (lpPerSocketContext == NULL)

    return NULL;


    // 할당된 구조체와 소켓을 g_hIOCP에 연결한다.

    g_hIOCP = CreateIoCompletionPort((HANDLE)sd, g_hIOCP,

    (DWORD)lpPerSocketContext, 0);

    if (NULL == g_hIOCP)

    {

    printf("CreateIoCompletionPort: %dn", GetLastError());

    if (lpPerSocketContext->pIOContext)

    free(lpPerSocketContext->pIOContext);

    free(lpPerSocketContext);

    return(NULL);

    }


    // 이 구조체를 링크드 리스트에 보관한다.

    CtxtListAddTo(lpPerSocketContext);

    return(lpPerSocketContext);

    }

    위의 코드를 보면 PER_SOCKET_CONTEXT 타입의 구조체를 할당하기 위해서 CtxtAllocate라는 함수를 사용하고 있다. 이 함수에 대해서는 뒤에서 다시 설명할 텐데 구조체를 할당하고 초기화하는 일을 담당한다. 그 다음에 CreateIoCompletionPort 함수를 이용해서 이 구조체와 소켓을 IOCP에 연결한다. 마지막으로 이렇게 생성된 구조체를 전체적으로 관리하기 위해서 CtxtListAddTo 함수를 호출한다. 이 함수 역시 뒤에서 다시 설명하겠다.


    --------------------------------------------------------------------------------

    참고 2. CreateIoCompletionPort

    이 함수의 원형은 다음과 같다.

    HANDLE CreateIoCompletionPort(HANDLE FileHandle,

    HANDLE ExistingCompletionPort,

    ULONG_PTR CompletionKey,

    DWORD NumberOfConcurrentThreads);

    첫 번째 인자인 FileHandle은 IOCP의 대상이 되는 입출력 객체의 핸들이어야 한다. 이 객체는 반드시 오버랩드 I/O 모드로 오픈된 것이어야 한다. 만일 이 인자의 값이 INVALID_FILE_HANDLE로 주어지고 두 번째 인자의 값이 NULL이 되면 이 함수의 리턴값은 새롭게 생성된 IOCP의 핸들이 된다. 이 때 세번째 인자의 값은 무시된다.

    두 번째 인자인 ExistingCompletionPort는 IOCP에 대한 핸들을 지정하기 위해 사용된다. 이 경우 첫번째 인자의 값은 입출력 객체의 핸들이 되어야 하며 이 둘은 연결되게 된다. 그런 경우 이 함수는 두번째 인자로 지정된 IOCP 핸들을 그대로 다시 리턴한다.

    세 번째 인자인 CompletionKey는 IOCP와 연결된 입출력 객체에 특정한 포인터라고 할 수 있다. 한 IOCP에는 여러 개의 입출력 객체가 동시에 연관될 수 있기 때문에 이 값을 통해 어느 객체로부터의 I/O 결과인지를 구분할 수 있다. 따라서 여러 개의 입출력 객체를 사용할 경우 이 인자는 아주 중요한 역할을 하게 된다.

    마지막 인자인NumberOfConcurrentThreads는 이 IOCP에 연관지어지는 스레드의 최대 수를 지정하는데 사용된다. 0을 주면 시스템의 자원이 허용하는 한 스레드가 계속 만들어지게 된다.


    --------------------------------------------------------------------------------

    4> 비동기 I/O의 수행

    앞 절차에서 소켓이 제대로 IOCP에 연결이 되고 나면 이제 그 소켓에 대해 비동기 I/O 작업을 수행해야 한다. 소켓의 경우, WSASend와 WSARead를 호출하면 그 결과는 g_hIOCP라는 것이 가리키는 큐안에 쌓이게 된다. 다음과 같은 함수들이 비동기 I/O 결과를 IOCP큐에 넣는다.

     ReadFile, WriteFile

     WSASend, WSARecv

     ConnectNamedPipe

     DeviceIoControl

     LockFileEx

     ReadDirectoryChanges

     TransactNamedPipe

     WaitCommEvent

    예제 프로그램에서는 UpdateCompletionPort 함수의 호출이 성공적으로 끝난 후에 클라이언트에서 보내는 데이터를 받기 위해서 WSARead 함수를 한번 호출한다. 참고로 다시 한번 이야기하자면 이 서버 프로그램은 에코우 서버이기 때문에 클라이언트가 보낸 데이터를 그대로 다시 클라이언트로 전송한다.

    lpPerSocketContext = UpdateCompletionPort(sdAccept, ClientIoRead);

    if (NULL == lpPerSocketContext)

    {

    CleanUp();

    return 1;

    }


    // 소켓에 비동기 읽기를 수행한다.

    nRet = WSARecv(sdAccept, &(lpPerSocketContext->pIOContext->wsabuf), 1,

    &dwRecvNumBytes, &dwFlags,

    &(lpPerSocketContext->pIOContext->Overlapped), NULL);

    if (nRet == SOCKET_ERROR && (ERROR_IO_PENDING != WSAGetLastError()))

    {

    printf("WSARecv Failed: %dn", WSAGetLastError());

    CloseClient(lpPerSocketContext);

    }

    } //while

    위의 WSARecv 함수 호출에서 6번째 인자를 눈여겨 보기 바란다. WSAOVERLAPPED 구조체의 변수를 지정하는데 PER_IO_CONTEXT의 Overlapped 필드를 넘기고 있다. 3>에서 설명한 것처럼 이는 사실 pIOContext의 주소를 넘기는 것과 동일한 효과를 갖는다.

    아무튼 WSARecv로 인한 읽기 작업이 완료되면 이는 IOCP 큐에 들어간다. 이를 읽어들이는 작업은 앞에서 만든 스레드들에서 수행한다. 이 함수는 비동기 함수이기 때문에 바로 리턴하고 그리고나서 코드는 다시 while 루프로 선두로 가서 다른 클라이언트로부터의 연결을 대기한다.

    while (g_bEndServer == FALSE)

    {

    // 클라이언트가 들어오기를 대기한다.

    sdAccept = WSAAccept(g_sdListen, NULL, NULL, NULL, 0);

    즉, main 함수는 초기화 작업을 하고 난 뒤부터는 클라이언트로부터의 소켓연결이 맺어지기를 기다렸다가 만들어지면 이를 IOCP와 연결한 뒤에 WSARecv를 한번 호출하는 일만 한다. 실제 작업은 모두 스레드에서 이루어진다.

    참고로 WSASend와 WSARecv의 함수 원형을 살펴보자.

    int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,

    LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags,

    LPWSAOVERLAPPED lpOverlapped,

    LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);


    int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,

    LPDWORD lpNumberOfBytesSent, DWORD dwFlags,

    LPWSAOVERLAPPED lpOverlapped,

    LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

    이 두 함수는 비슷한 인자를 많이 갖고 있다. 먼저 모두 첫번째 인자는 소켓 핸들이다. 두 번째 인자는 WSABUF라는 구조체에 대한 포인터로 보낼 데이터에 대한 정보이거나 데이터를 받을 버퍼에 대한 정보이다. WSABUF는 다음과 같이 버퍼의 시작 주소와 버퍼의 크기를 지정하는 두개의 필드로 구성되어 있다.

    Typedef struct __WSABUF

    {

    u_long len; // 버퍼 크기

    char FAR *buf; // 버퍼 시작 주소

    } WSABUF, FAR *LPWASBUF;

    이 두 번째 인자로는 WSABUF 배열의 주소를 지정할 수도 있다. 그 경우 차례로 여러 버퍼의 데이터를 전송하거나 (WSASend의 경우) 받은 데이터를 여러 버퍼로 옮기는 역할(WSARecv의 경우)을 한다. 세 번째 인자는 이 두 번째 인자가 가리키는 WSABUF 변수의 수를 나타낸다. 배열을 지정했을 경우에는 그 크기를 이 인자로 지정해주면 된다. 배열이 아니라면 그냥 1을 지정하면 된다. 여기서 한가지 알아야 할 점은 이 두 함수 모두 지정한 크기만큼 입출력이 종료된 다음에 리턴되는 것이 아니란 점이다. WSARecv 같은 경우에는 읽어올 데이터가 생기면 지정된 크기와 관계없이 바로 작업을 종료한다. WSASend의 경우에는 소켓 버퍼가 꽉 차서 데이터를 지정된 크기만큼 보낼 수 없으면 일단 보낼 수 있는 만큼 보내고 만다.

    네 번째 인자는 각기 실제로 전송된 데이터(WSASend의 경우)와 실제로 읽어들인 데이터(WSARecv의 경우)의 크기가 들어간다. 그런데 이 함수들을 예제 프로그램에서처럼 비동기 모드로 사용할 경우에는 이 인자로 리턴되는 값은 함수 자체의 리턴값이 0인 경우에만 의미가 있다. 0인 경우는 바로 작업이 끝난 경우이다. 함수가 바로 끝나지 않을 경우에는 SOCKET_ERROR가 리턴되고 이 때 GetLastError 함수를 호출해보면 그 값이 WSA_IO_PENDING일 것이다.

    다섯 번째 인자는 약간 복잡한데 일단 대부분 0이 리턴되거나 (WSARecv의 경우) 0이 지정(WSASend의 경우)된다고 알아두기 바란다. 여섯 번째 인자는 WSAOVERLAPPED 구조체에 대한 포인터를 지정하는 부분이다. IOCP를 사용하는 경우에는 hEvent 필드의 값은 반드시 NULL이 지정되어야 한다. 마지막 인자는 콜백함수를 지정하는데 사용된다. 이 콜백함수의 원형은 다음과 같다.

    void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred,

    LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);

    만일 여섯 번째 인자와 마지막 인자가 모두 NULL이면 이 함수들은 동기 모드로 동작한다. 여섯 번째 인자와 마지막 인자가 모두 지정되면 작업이 종료되었을 때 마지막 인자로 지정된 함수가 호출된다. 여섯 번째 인자만 지정되고 첫 번째 인자로 지정된 소켓이 IOCP와 연결되어 있으면 이 함수의 결과는 IOCP 큐에 들어간다. 사실 이 두 함수의 인자들을 제대로 이해할 수 있다면 윈도우 운영체제의 입출력 함수는 다 이해했다고 봐도 무방하다.

    5> 비동기 I/O 결과 읽기

    앞서 수행된 비동기 I/O의 결과를 읽어들이려면 GetQueuedCompletionPort라는 함수를 이용해야 한다. 이 함수 원형에 대한 설명은 참고 3에 있다. 이 함수는 IOCP 큐안에 읽어들일 비동기 I/O 결과가 있으면 이를 읽어가지고 리턴한다. 읽어올 것이 없으면 읽어올 것이 생길 때까지 리턴하지 않는다. 다음 코드처럼 이 함수는 무한루프안에서 계속적으로 호출되는 것이 일반적이다.

    While (1)

    {

    GetQueuedCompletionStatus(…);

    // 읽어들인 결과를 바탕으로 다음 일을 수행한다.

    }

    예제 프로그램과 같은 에코우 서버에서는 특정 소켓에 대해 읽기 작업이 완료된 결과를 읽어들였으면 이를 비동기로 쓰는 작업을 하고, 쓰기 작업이 완료된 결과를 읽어들였으면 다시 비동기로 읽기 작업을 수행한다. 앞서 이야기한 것처럼 GetQueuedCompletionPort 함수의 세 번째 인자로는 현재 이 소켓에 대해 따로 할당된PER_SOCKET_CONTEXT 구조체의 포인터가 리턴되고 이 것의 pIOContext 필드를 보면 현재 진행중인 작업의 상태를 알 수 있다. pIOContext의IOOperation 필드의 값이ClientIoRead이면 지금 큐에서 읽어온 작업이 읽기 작업의 결과인 것이고 ClientIoWrite이면 쓰기 작업인 것이다.

    위의 코드를 좀더 예제 프로그램에 맞게 고쳐보면 다음과 같은 식이다.

    While (1)

    {

    GetQueuedCompletionStatus(…);

    // 읽어들인 결과를 바탕으로 다음 일을 수행한다.

    만일 읽어들인 결과가 읽기 작업이면

    읽어들인 데이터를 그대로 다시 서버로 보낸다 (물론 비동기 I/O)

    만일 읽어들인 결과가 쓰기 작업이면

    만일 앞서 쓰기 요청한 것이 다 전송되지 않았으면

    전송안 된 부분만 다시 전송한다

    다 전송되었으면

    읽기 비동기 작업을 소켓에 수행한다.

    }


    --------------------------------------------------------------------------------

    참고 3. GetQueuedCompletionStatus

    이 함수의 원형은 다음과 같다.

    BOOL GetQueuedCompletionStatus(

    HANDLE CompletionPort,

    LPDWORD lpNumberOfBytes,

    PULONG_PTR lpCompletionKey,

    LPOVERLAPPED *lpOverlapped,

    DWORD dwMilliseconds);

    첫 번째 인자인 CompletionPort로는 앞서 생성된 IOCP 객체의 핸들을 지정한다.

    두 번째 인자로는 지금 읽어온 I/O 작업의 결과로 읽거나 쓴 데이터의 크기가 바이트 단위로 지정된다. 즉 이 인자의 값은 운영체제에서 지정한다.

    세 번째 인자인 lpCompletionKey역시 운영체제에 의해 채워져 리턴되는 값이다. CreateIoCompletionPort 함수로 IOCP 객체를 생성할 때 세 번째 인자로 지정한 값이 여기로 리턴된다. 앞서 이야기한 것처럼 한 IOCP 객체로 둘 이상의 입출력 디바이스를 처리할 수 있기 때문에 이를 구분하는 값이 여기로 지정된다고 생각하면 된다.

    네 번째 인자인 lpOverlapped 역시 운영체제에 의해 값이 지정되는데 이는 한 입출력 디바이스내에서 각각의 입출력 작업을 구별하는 역할을 한다. 이 값은 사실 앞서 비동기 작업에서 사용된 OVERLAPPED 구조체의 주소가 그대로 들어온다. 그렇기 때문에 비동기 I/O 작업시에 OVERLAPPED 구조체를 스택에 있는 것을 사용하면 안 되고 각 작업마다 서로 다른 OVERLAPPED 구조체가 사용되어야 하는 것이다.

    마지막 인자인dwMilliseconds는 IOCP 큐에 결과가 없을 경우 얼마나 더 대기하다가 리턴할 것인지를 밀리세컨드 단위로 지정한다. 만일 타임아웃이 나서 리턴할 경우에는 GetQueuedCompletionStatus 함수의 리턴값은 FALSE가 되고 네 번째인자로는 NULL이 지정된다. 읽어올 것이 생길 때까지 대기하도록 하고 싶으면 이 인자로 INFINITE를 지정하면 된다.

    위의 플로우를 염두에 두고 이제 예제 프로그램의 스레드 코드를 실제로 살펴보자. 주석을 자세히 달아놓았으므로 주석과 함께 코드를 살펴보기 바란다.

    DWORD WINAPI EchoThread (LPVOID WorkThreadContext)

    {

    // 앞서 스레드 생성시 스레드 함수의 인자로 IOCP 핸들을 지정했었다.

    // 인자를 IOCP 핸들로 캐스팅한다.

    HANDLE hIOCP = (HANDLE)WorkThreadContext;

    BOOL bSuccess = FALSE;

    int nRet;

    LPOVERLAPPED lpOverlapped = NULL;

    PPER_SOCKET_CONTEXT lpPerSocketContext = NULL;

    PPER_IO_CONTEXT lpIOContext = NULL;

    WSABUF buffRecv;

    WSABUF buffSend;

    DWORD dwRecvNumBytes = 0;

    DWORD dwSendNumBytes = 0;

    DWORD dwFlags = 0;

    DWORD dwIoSize;

    while (TRUE)

    {

    // IOCP 큐에서 비동기 I/O 결과를 하나 읽어온다.

    bSuccess = GetQueuedCompletionStatus(hIOCP, &dwIoSize,

    (LPDWORD)&lpPerSocketContext, &lpOverlapped,INFINITE);

    if (!bSuccess)

    printf("GetQueuedCompletionStatus: %dn", GetLastError());


    // CleanUp 함수에 의해서 스레드의 강제 종료 명령이 내려지면..

    if (lpPerSocketContext == NULL) return 0;

    if (g_bEndServer) return 0;


    // 클라이언트와의 소켓 연결이 끊어졌으면…

    if (!bSuccess || (bSuccess && (0 == dwIoSize)))

    {

    // lpPerSocketContext를 메모리에서 제거한다.

    CloseClient(lpPerSocketContext);

    continue;

    }


    /* 앞서 WSASend와 WSARecv에 의해 I/O 작업을 할 때 넘겼던 WSAOVERLAPPED

    타입의 변수가 사실은 PER_IO_CONTEXT 타입의 시작이기도 하므로 이를 캐스팅하

    여 사용가능하다. */

    lpIOContext = (PPER_IO_CONTEXT)lpOverlapped;

    switch (lpIOContext->IOOperation) // 끝난 작업 종류가 무엇인가 ?

    {

    case ClientIoRead: // 읽기 작업인가 ?

    // --------------------------------------------

    // 받은 것을 그대로 보낸다. 즉, 다음 작업은 쓰기 작업이다.

    // --------------------------------------------

    printf("%s를 받았고 이를 재전송합니다.n.", lpIOContext->wsabuf.buf);

    lpIOContext->IOOperation = ClientIoWrite; // 이제 쓰기 작업이 진행됨을 표시

    // 얼마큼 전송할 것인지 명시한다. 받은 만큼 보낸다. 이는 상태를 기록하기

    // 위함이지 WSASend 함수와는 관련없다.

    lpIOContext->nTotalBytes = dwIoSize;

    // 전송된 데이터 크기. 아직 보낸 것이 없으므로 0

    lpIOContext->nSentBytes = 0;

    // WSASend에게 보낼 데이터의 포인터와 크기를 지정한다.

    // 받은 데이터가 이미 lpIOContext->wsabuf.buf에 있다.

    lpIOContext->wsabuf.len = dwIoSize; // 크기 지정

    dwFlags = 0;

    nRet = WSASend(lpPerSocketContext->Socket,

    &lpIOContext->wsabuf, 1, &dwSendNumBytes,

    dwFlags, &(lpIOContext->Overlapped), NULL);

    if (SOCKET_ERROR == nRet && (ERROR_IO_PENDING != WSAGetLastError()))

    {

    printf("WSASend: %dn", WSAGetLastError());

    CloseClient(lpPerSocketContext);

    }

    break;


    case ClientIoWrite: // 쓰기 작업인가 ?

    // ----------------------------------------------------

    // 전송이 다 되었는지 확인한다. 다 전송되지 않았으면 아직 전송되지

    // 않은 데이터를 다시 보낸다. 다 전송되었으면 WSARecv를 호출해서

    // 다시 받기 모드로 진입한다.

    // --------------------------------------------

    lpIOContext->nSentBytes += dwIoSize; // 전송된 데이터 크기 업데이트

    dwFlags = 0;

    if (lpIOContext->nSentBytesnTotalBytes) // 다 전송되지 않았으면

    {

    // 마저 전송해야 하므로 아직 보내기모드

    lpIOContext->IOOperation = ClientIoWrite;

    // -----------------------

    // 전송되지 않은 부분을 보낸다.

    // -----------------------

    // 버퍼 포인터를 업데이트하고

    buffSend.buf = lpIOContext->Buffer + lpIOContext->nSentBytes;

    // 보내야할 데이터의 크기를 남은 데이터의 크기만큼으로 줄인다.

    buffSend.len = lpIOContext->nTotalBytes - lpIOContext->nSentBytes;

    nRet = WSASend (lpPerSocketContext->Socket,

    &buffSend, 1, &dwSendNumBytes,

    dwFlags, &(lpIOContext->Overlapped), NULL);

    // SOCKET_ERROR가 리턴된 경우에는 반드시 WSAGetLastError의 리턴값이

    // ERROR_IO_PENDING이어야 한다.

    if (SOCKET_ERROR == nRet && (ERROR_IO_PENDING != WSAGetLastError()))

    {

    printf ("WSASend: %dn", WSAGetLastError());

    CloseClient(lpPerSocketContext);

    }

    }

    else // 데이터가 전부 전송된 경우

    {

    // 다시 이 소켓으로부터 데이터를 받기 위해 WSARecv를 호출한다.

    lpIOContext->IOOperation = ClientIoRead;

    dwRecvNumBytes = 0;

    dwFlags = 0;

    buffRecv.buf = lpIOContext->Buffer; // 수신버퍼 지정

    // 읽어들일 데이터 크기 지정. 사실 이 크기만큼 데이터를 읽어들여야

    // 그 결과가 IOCP큐에 들어가는 것은 아니다. 이 크기 이상 안

    // 읽어들일 뿐이고 데이터가 이용가능한 만큼 IOCP큐에 넣는다.

    buffRecv.len = MAX_BUFF_SIZE;

    nRet = WSARecv(lpPerSocketContext->Socket,

    &buffRecv, 1, &dwRecvNumBytes,

    &dwFlags, &(lpIOContext->Overlapped), NULL);

    // SOCKET_ERROR가 리턴된 경우에는 반드시 WSAGetLastError의 리턴값이

    // ERROR_IO_PENDING이어야 한다.

    if (SOCKET_ERROR == nRet && (ERROR_IO_PENDING != WSAGetLastError()))

    {

    printf ("WSARecv: %dn", WSAGetLastError());

    CloseClient(lpPerSocketContext);

    }

    }

    break;

    } //switch

    } //while

    return(0);

    }

    자 이상으로 IOCP가 어떤 식으로 동작하는지 알아보았다. 단계별로 설명과 코드를 잘 살펴보면 어떻게 동작하는지 더 쉽게 이해할 수 있을 것이다.

    3. 예제 프로그램의 기타 코드 설명

    예제 프로그램에서 설명이 안 된 코드는 서버와 연결된 클라이언트의 리스트를 관리하는 함수들(CtxtAllocate, CtxtListFree, CtxtListAddTo, CtxtListDeleteFrom)과 청소 함수(CleanUp, CloseClient), 대기 소켓 생성함수(CreateListenSocket)등이다. 대기 소켓 생성 함수는 이미 지난 연재에서 살펴본 내용(사실 socket 대신에 WSASocket을 호출하는 부분만 다르다)이기 때문에 여기서는 다른 함수들에 대해서만 알아보겠다.

    클라이언트 리스트 관리 함수들

    접속하는 클라이언트가 생길 때마다 이는g_CtxtList에 기록된다. 이는CptrList 타입의 링크드 리스트 클래스이고 이 변수로의 접근은 모두g_CriticalSection이란 크리티컬 섹션에 의해 한번에 한 스레드로 제한된다.

    CtxtAllocate는 인자로 지정된 소켓에 PER_SOCKET_CONTEXT 구조체를 하나 할당하고 그 구조체를 초기화한 다음에 이를 리턴한다. 할당에 실패하면 NULL을 리턴한다. PER_SOCKET_CONTEXT 구조체의 IO_PER_CONTEXT 타입의 필드인 pIOContext의 필드를 초기화하는 부분을 눈여겨 봐두기 바란다.

    PPER_SOCKET_CONTEXT CtxtAllocate(SOCKET sd, IO_OPERATION ClientIO)

    {

    PPER_SOCKET_CONTEXT lpPerSocCon;


    lpPerSocCon = (PPER_SOCKET_CONTEXT)malloc(sizeof(PER_SOCKET_CONTEXT));

    if (lpPerSocCon)

    {

    lpPerSocCon->pIOContext = (PPER_IO_CONTEXT)

    malloc(sizeof(PER_IO_CONTEXT));

    if (lpPerSocCon->pIOContext)

    {

    lpPerSocCon->Socket = sd;

    memset(&lpPerSocCon->pIOContext->Overlapped,

    0, sizeof(OVERLAPPED));

    lpPerSocCon->pIOContext->IOOperation = ClientIO;

    lpPerSocCon->pIOContext->nTotalBytes = 0;

    lpPerSocCon->pIOContext->nSentBytes = 0;

    lpPerSocCon->pIOContext->wsabuf.buf = lpPerSocCon->pIOContext->Buffer;

    lpPerSocCon->pIOContext->wsabuf.len = MAX_BUFF_SIZE;

    }

    else

    {

    free(lpPerSocCon);

    lpPerSocCon = NULL;

    }

    }

    return(lpPerSocCon);

    }

    나머지 세 함수들은 간단하다. CptrList 클래스를 사용해본 이라면 이 함수들의 소스를 이해하기가 아주 쉬울 것이다. 여기서는 CtxtListAddTo와 CtxtListDeleteFrom 함수만 살펴보겠다.

    // g_CtxtList에 lpPerSocketContext가 가리키는 항목을 추가한다

    VOID CtxtListAddTo (PPER_SOCKET_CONTEXT lpPerSocketContext)

    {

    EnterCriticalSection(&g_CriticalSection);

    g_CtxtList.AddTail(lpPerSocketContext); // 리스트의 끝에 붙인다.

    LeaveCriticalSection(&g_CriticalSection);

    return;

    }


    // g_CtxtList에서 lpPerSocketContext가 가리키는 항목을 제거한다.

    VOID CtxtListDeleteFrom(PPER_SOCKET_CONTEXT lpPerSocketContext)

    {

    EnterCriticalSection(&g_CriticalSection);

    if (lpPerSocketContext)

    {

    POSITION pos = g_CtxtList.Find(lpPerSocketContext);

    if (pos)

    {

    g_CtxtList.RemoveAt(pos);

    if (lpPerSocketContext->pIOContext)

    free(lpPerSocketContext->pIOContext);

    free(lpPerSocketContext);

    }

    }

    LeaveCriticalSection(&g_CriticalSection);

    return;

    }

    청소 함수들

    여기서는 CleanUp 함수의 코드를 보기로 하겠다. 이 함수를 프로그램이 종료될 때 호출되는 함수로 모든 스레드가 종료되기를 기다렸다가 클라이언트 리스트에 할당되었던 자료구조들을 제거하고 최종적으로 IOCP와 대기 소켓을 제거하는 일을 수행한다.

    void CleanUp()

    {

    if (g_hIOCP)

    {

    // 스레드를 강제 종료하도록 한다.

    // 참고 4와 EchoThread의 if (lpPerSocketContext == NULL)를 같이 보기 바란다.

    for (DWORD i = 0; i < g_dwThreadCount; i++)

    PostQueuedCompletionStatus(g_hIOCP, 0, 0, NULL);

    }


    // 모든 스레드가 실행을 중지했는지 확인한다.

    if (WAIT_OBJECT_0 != WaitForMultipleObjects( g_dwThreadCount, g_hThreads,

    TRUE, 1000))

    printf("WaitForMultipleObjects failed: %dn", GetLastError());

    else

    for (DWORD i = 0; i < g_dwThreadCount; i++) // 스레드 핸들을 모두 닫는다.

    {

    if (g_hThreads[i] != INVALID_HANDLE_VALUE) CloseHandle(g_hThreads[i]);

    g_hThreads[i] = INVALID_HANDLE_VALUE;

    }

    // g_CtxtList에 들어있는 클라이언트들을 모두 제거한다.

    CtxtListFree();

    // IOCP를 제거한다.

    if (g_hIOCP)

    {

    CloseHandle(g_hIOCP);

    g_hIOCP = NULL;

    }

    // 대기 소켓을 제거한다.

    if (g_sdListen != INVALID_SOCKET)

    {

    closesocket(g_sdListen);

    g_sdListen = INVALID_SOCKET;

    }


    DeleteCriticalSection(&g_CriticalSection); // 크리티컬 섹션을 제거한다.

    WSACleanup(); // 윈속 라이브러리를 해제한다.

    }


    --------------------------------------------------------------------------------

    참고 4. PostQueuedCompletionPort

    앞에서 설명한 것처럼 이 함수는 IOCP 큐에 마치 비동기 작업이 끝나서 그 결과가 큐에 들어가는 것처럼 흉내내는 기능을 한다. 그렇기 때문에 이 함수의 인자들을 보면 GetQueuedCompletionStatus 함수에 있는 것과 동일하다. 이 함수의 원형은 다음과 같다.

    BOOL PostQueuedCompletionStatus(

    HANDLE CompletionPort,

    DWORD dwNumberOfBytesTransferred,

    ULONG_PTR dwCompletionKey,

    LPOVERLAPPED lpOverlapped);

    첫 번째 인자인 CompletionPort로는 지금 만들어내는 I/O 작업의 결과가 들어갈 IOCP 객체의 핸들을 지정한다.

    두 번째 인자인 dwNumberOfBytesTransferred는 GetQueuedCompletionStatus 함수의 두 번째 인자로 넘어갈 값을 지정한다.

    세 번째 인자인 dwCompletionKey는 두 번째 인자와 마찬가지로 GetQueuedCompletionStatus 함수의lpCompletionKey 인자로 들어갈 값을 지정하는데 사용된다.

    네 번째 인자인 lpOverlapped는 앞서 인자들과 마찬가지로 GetQueuedCompletionStatus 함수의 네 번째 인자로 들어갈 OVERLAPPED 구조체의 값을 넘긴다.

    이 함수가 성공적으로 인자로 지정된 값들을 IOCP 큐에 넣으면 0이 아닌 값이 리턴된다. 실패시에는 0이 리턴되며 이 때는 GetLastError 함수를 호출해서 에러의 원인을 찾아볼 수 있다.

    예제 프로그램의 실행 화면은 그림 2와 같다.

    < 그림 2. 예제 프로그램의 실행화면 >

    이 것으로 IOCP에 대한 장황한 설명을 마치겠다. 아마 이해하기가 그리 쉽지 않을 것이다. 필자의 경우에도 이를 이해하는데 상당한 시간을 소모했으며 위의 예제 프로그램을 바탕으로 실제 환경하에서 동작하는 프로그램을 만드는데도 상당한 시간을 보냈다. 이해하기는 어렵지만 IOCP는 스레드을 최대한으로 활용할 수 있도록 해주는 메커니즘이다. 특히 소켓으로 다중 사용자의 요구를 처리해야 하는 프로그램을 만들어야 한다면 IOCP는 최적의 솔루션이 아닌가 싶다.

    참고문헌
    1. INFO: Design Issues When Using IOCP in a Winsock Server (Q192800) - http://support.microsoft.com/default.aspx?scid=kb;EN-US;q192800
    2. Programming Server-Side Applications for Microsoft Windows 2000, Chapter 2 Devico I/O and Interthreaded Communication
    3. Writing Windows NT Server Applications in MFC Using I/O Completion Ports - http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpic/html/msdn_servrapp.asp
    4. UNBUFCPY, SOCKSRV – Microsoft Platform SDK IOCP 윈속 예제 프로그램
    5. Windows Sockets 2.0: Write Scalable Winsock Apps Using Completion Ports - http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnmag00/html/Winsock.asp

    출처 : http://devdev.tistory.com/132

    + Recent posts