Mandoo’s WLAN story

[C언어] 서버-클라이언트 통신: 패킷 손실 문제의 원인 및 해결방법 본문

Mandoo's IT Story/Dev

[C언어] 서버-클라이언트 통신: 패킷 손실 문제의 원인 및 해결방법

mandoo12 2023. 7. 26. 12:06

서버와 클라이언트가 소켓으로 패킷을 전달하는 과정에서 패킷이 손실되는 문제가 발생했고,

해결방법을 정리해보았다.

 

해결하는 과정을 세세하게 적어두었으니 부분부분 넘어가도 좋다.

 

▶ 통신 환경
 통신 : 소켓통신
 서버 : 웹 (웹 개발자가 따로 있어서 사용언어는 정확하지 않지만, JAVA로 추측됨)
 클라이언트 : C언어로 개발된 리눅스용 프로그램
 패킷 : 헥사값으로 이루어진 5000byte 이상의 패킷

 

▶ 문제점
 클라이언트가 패킷 전체를 전달받지 못하는 문제가 간헐적으로 발생

 

▶ 원인 파악 과정

 1. 네트워크 상태 체크 : 클라이언트 - 서버 ping체크
 2. 클라이언트 C 소스코드에서 request 패킷 전송 전, 후 / response 패킷 수신 전, 후 마다 연결 상태 및 에러를 체크했다.

 3.  서버에서 패킷을 전송하는 시점부터 클라이언트가 수신하는 과정을 wireshark로 확인해보았다.

 

 원인 파악
wireshirk로 확인 시 패킷을 여러번 쪼개서 보냄을 확인했다.

이 때 패킷을 1464byte로 잘라서 총 4번을 보내고 있다는 것을 확인했고, 부가적인 설명은 아래와 같다.

 

 - 클라이언트에서 패킷을 받을 때에는 Info 필드의 Len에서 확인되는 1398byte씩 총 4번씩 패킷을 수신하게 된다.

 - 아래 사진 info 필드에 Sequence 넘버와 Len을  표시해두었다.

 - 패킷이 전송될 때 붙었던 헤더들을 제외하고 실제로 서버에서 보낸 패킷만 전달받는다.

 - wireshark 우측 하단에 보면 헥사값을 십진수로 나타낸 패킷 내용을 보면 패킷이 잘렸음을 확실하게 확인 할 수 있다.

 

 - 해당 문제는 서버담당자에게 확인해보니, 서버의 MTU의 크기가 1500으로 설정되어있다고 한다.

 - 사내 서버망 문제로 MTU 설정이 불가능하다고 해서 현재 상태로 유지하기로 했다.

 - MTU는 네트워크의 최대 전송 단위이고, 따로 설정하지 않으면 기본값은 1460이며,

   1300 ~ 8896 사이의 모든 값 으로 설정할 수 있다.

   (MTU 설정하는 방법은 구글에 검색하면 쉽게 찾아볼 수 있다.)

 

아래 사진을 보면 서버에서 패킷이 전송 될 때 네트워크 상에서 4개의 패킷으로 쪼개서 전송됐지만,

클라이언트가 보낸 ACK 패킷은 2개만 존재하고, ACK 넘버가 1부터 시작을 하지 않은 상태임을 확인 할 수 있다.

정상적인 경우에는 아래 캡쳐와 같이 클라이언트에서 ACK 패킷을 총 4번 보내주어야 한다.

 

 클라이언트 해결방법

근본적인 원인을 알았으니 이제 클라이언트에서 처리를 해주어야한다.

처음에 작성했던 패킷을 수신받는 부분 코드는 3번째줄이다.

read를 한번만 실행해주도록 했다.

write(sockfd, write_buff, TX_BUFF_SIZE);

int read_len = 0;
rx_len = read(sockfd, read_buff, RX_BUFF_SIZE);

if(rx_len<=0)
{
    close(sockfd);
    printf("\n read packet error!!\n");
}

 

수정한 코드는 아래 코드와 같다

#define REAL_LEN 6310

unsigned char read_buff[5000]={0x00,}; //서버로부터 전달받은 패킷을 저장할 변수
int read_len = 0; //read 함수의 반환값을 저장 (반환값 : 패킷의 길이)

read_len = read(sockfd, read_buff, RX_BUFF_SIZE); //서버에서 보낸 패킷을 수신
		
if(read_len != REAL_LEN) //실제로 read에서 받아야 하는 패킷 사이즈가 아닐 시 아래 코드 수행
{
	unsigned char tmp_buff[1398]={0x00,}; //남은 패킷을 반복해서 저장할 변수
	int read_len1 = 0; //다시 read할 때의 반환값을 저장 할 변수
	
	unsigned char tmp_read_buff1[5000]={0x00,}; //다시 read한 패킷을 한 공간에 이어붙힐 때 사용 할 변수
	int tmp_read_buff_len = 0; //남은 패킷들의 사이즈를 합산
	
	int div_read_len = read_len/1398; //다시 read 할 횟수 정의.
	
	int tmp_len = REAL_LEN - read_len;

	if((tmp_len % 1398) != 0) //1398로 나눈 상태에서 나머지가 있을 시 +1
		div_read_len++;

	for(int i = 0; i < div_read_len; i++) //div_read_len 만큼 read를 반복
	{
		read_len1 = read(sockfd, tmp_buff, sizeof(tmp_buff));
		
		if(read_len1 > 0) //read 로 패킷을 받았을때만 read_buff에 이어붙힌다.
		{
			memcpy(read_buff+read_len, tmp_buff, sizeof(tmp_buff));
			read_len += read_len1;
		}
	}
}

1. 처음에 read 할 때 1398byte로 쪼개진 4개의 패킷을 다 받을수도 있고, 몇개만 받고 끝날수도 있음

2. 실제로 서버에서 전송한 패킷의 사이즈와 현재 read로 받은 패킷의 사이즈를 비교

3. 처음에 수신한 read_len이 실제 패킷의 길이와 다르다면 못받은 패킷의 개수만큼  read를 다시 해준다.

 * read를 다시 해줄 횟수를 구하는 조건은 아래와 같다

  - 원래 받아야 하는 패킷 사이즈 : REAL_LEN

  - 처음 read 시 실제로 받은 패킷 사이즈 : read_len

  - 몇 byte 기준으로 쪼개지고 있는지 : REAL_LEN 

  - 받아오지 못한 패킷 사이즈 : tmp_len = REAL_LEN - read_len

  - 반복해야하는 횟수 : div_read_len = tmp_len / REAL_LEN 

  - 나머지가 있는 경우 : div_read_len++ 

     -> 마지막에 남은 패킷의 사이즈가  1398보다 작은 값이 될 수 있기 때문에 +1이 되어야 함.

  - div_read_len 만큼 read를 반복해서 수행

4. read를 반복 할 때 마다 처음에 read한 패킷이 저장되어있는 read_buff에 값을 쌓아준다.

 * read_len에도 다시 받은 패킷의 길이를 누적한다.

 

패킷을 전달받는 부분만 따로 올리려고 임의로 코드를 작성했기 때문에, 문제가 있을 수 있으니 흐름만 보고 복붙해서 쓰는건 추천하지 않는다.

 

 요약해보면

나는 위의 해결방법과 같이 실제로 받아야하는 패킷의 사이즈를 알고있는 상태에서 하드코딩해서 패킷 전체를 수신하였다.

위 원인파악 부분에서 설명했듯이 패킷이 쪼개지는 기준은 1398byte씩 동일하게 쪼개졌고, 이는 서버의 MTU로 인해 발생한 문제이다.

서버 세팅을 바꿀 수 없으니, 코드 안에서 패킷을 반복하여 받도록했다.

 

지금까지 문제없이 사용됐는데 개발환경이 바뀌면서 갑자기 발생한 이슈에 급하게 해결하느라 위와 같이 복잡한 계산을 통해 해결 할 수 밖에 없었다.

 

p.s 혹시라도 글에 문제점 or 궁금한 점 or 더 좋은 방법 이 있다면 댓글로 공유 부탁드립니다!

 

이만 뿅!