본문 바로가기
DSP, MCU/STM32 (ARM Cortex-M)

STM32 ] TCP Server, lwIP Raw API

by eteo 2022. 10. 2.

 

 

아래 헤더파일과 소스파일을 프로젝트에 포함시킨다.

 

tcp_echoserver.h
0.00MB
tcp_echoserver.c
0.01MB

 

STM324x9I_EVAL 공식 LwIP 예제에 포함된 파일로 tcp echo server 는 tcp client 에서 보낸걸 tcp server 가 그대로 다시 보내는 예제이다.

 

자세한 설명은 주석처리해두었다.

 

 

main.c

/* USER CODE BEGIN Includes */
#include "tcp_echoserver.h"
/* USER CODE END Includes */

//...

  /* USER CODE BEGIN 2 */
  tcp_echoserver_init();
  /* USER CODE END 2 */
  
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  MX_LWIP_Process();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

 

 

 

 

void tcp_echoserver_init(void) 함수

/**
  * @brief  Initializes the tcp echo server
  * @param  None
  * @retval None
  */
void tcp_echoserver_init(void)
{
  /* create new tcp pcb */
  tcp_echoserver_pcb = tcp_new();

  if (tcp_echoserver_pcb != NULL)
  {
    err_t err;
    
    /* bind echo_pcb to port 7 (ECHO protocol) */
    err = tcp_bind(tcp_echoserver_pcb, IP_ADDR_ANY, 7);
    
    if (err == ERR_OK)
    {
      /* start tcp listening for echo_pcb */
      tcp_echoserver_pcb = tcp_listen(tcp_echoserver_pcb);
      
      /* initialize LwIP tcp_accept callback function */
      tcp_accept(tcp_echoserver_pcb, tcp_echoserver_accept);
    }
    else 
    {
      /* deallocate the pcb */
      memp_free(MEMP_TCP_PCB, tcp_echoserver_pcb);
    }
  }
}

 

1. 함수를 통해 tcp control block을 생성한다.

tcp_echoserver_pcb = tcp_new();

2. 생성된 블락과 IP Address, 포트를 묶는다.

err = tcp_bind(tcp_echoserver_pcb, IP_ADDR_ANY, 7);

3. 클라이언트를 기다리는 listen 모드로 전환한다.

tcp_echoserver_pcb = tcp_listen(tcp_echoserver_pcb);

4. 서버에 의해 커넥션이 이뤄지면 호출될 콜백함수를 지정한다.

tcp_accept(tcp_echoserver_pcb, tcp_echoserver_accept);

 

만약 에코서버로 쓰는게 아니라면 다른 포트를로 바꿔도 되고 고정 ip로 설정하고 싶다면 아래처럼 쓰면 된다.

	ip_addr_t myIPADDR;
	IP_ADDR4(&myIPADDR, 192, 168, 0, 123);
	err = tcp_bind(tpcb, &myIPADDR, 12345);

 

 

 

 

 

 

 

static err_t tcp_echoserver_accept(void *arg, struct tcp_pcb *newpcb, err_t err) 함수

/**
  * @brief  This function is the implementation of tcp_accept LwIP callback
  * @param  arg: not used
  * @param  newpcb: pointer on tcp_pcb struct for the newly created tcp connection
  * @param  err: not used 
  * @retval err_t: error status
  */
static err_t tcp_echoserver_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
  err_t ret_err;
  struct tcp_echoserver_struct *es;

  LWIP_UNUSED_ARG(arg);
  LWIP_UNUSED_ARG(err);

  /* set priority for the newly accepted tcp connection newpcb */
  // 새로 accepted 된 tcp 커넥션 블락인 new pcb의 우선순위 설정
  tcp_setprio(newpcb, TCP_PRIO_MIN);

  /* allocate structure es to maintain tcp connection informations */
  // tcp 커넥션 정보 관리용 구조체 변수 es 동적할당
  es = (struct tcp_echoserver_struct *)mem_malloc(sizeof(struct tcp_echoserver_struct));
  if (es != NULL)
  {
  // ACCEPTED 상태로 변경
    es->state = ES_ACCEPTED;
    es->pcb = newpcb;
    es->retries = 0;
    es->p = NULL;
    
    // 각각 tcp 커넥션 블락에 대한 콜백이 호출될 때마다 해당 es가 인자로 전달되도록 함
    /* pass newly allocated es structure as argument to newpcb */
    tcp_arg(newpcb, es);
    
    /* initialize lwip tcp_recv callback function for newpcb  */ 
    tcp_recv(newpcb, tcp_echoserver_recv);
    
    /* initialize lwip tcp_err callback function for newpcb  */
    tcp_err(newpcb, tcp_echoserver_error);
    
    /* initialize lwip tcp_poll callback function for newpcb */
    tcp_poll(newpcb, tcp_echoserver_poll, 0);
    
    ret_err = ERR_OK;
  }
  else
  {
    /*  close tcp connection */
    tcp_echoserver_connection_close(newpcb, es);
    /* return memory error */
    ret_err = ERR_MEM;
  }
  return ret_err;  
}

 

함수 내부에서 tcp_arg(), tcp_recv(), tcp_err(), tcp_poll() 함수 등을 호출한다.

 

// tcp_recv 콜백함수에 전달될 인자 지정
void tcp_arg(struct tcp_pcb *pcb, void *arg)

// 수신 시 콜백함수 지정
void 	tcp_recv (struct tcp_pcb *pcb, tcp_recv_fn recv)

// 에러 발생시 콜백함수 지정
void 	tcp_err (struct tcp_pcb *pcb, tcp_err_fn err)

// 주기적으로 상태를 확인하기위해 Polling 콜백함수 지정
void 	tcp_poll (struct tcp_pcb *pcb, tcp_poll_fn poll, u8_t interval)

 

참고로 tcp_poll()은 세번째 파라미터인 interval*0.5초마다 호출된다.

 

 

그리고 그중에 tcp_recv() 함수를 통해 설정한 tcp_server_recv 콜백함수가 클라이언트로부터 데이터를 수신할 때마다 호출되는 콜백함수이다.

    /* initialize lwip tcp_recv callback function for newpcb  */
    tcp_recv(newpcb, tcp_server_recv);

 

 

 

 

static err_t tcp_echoserver_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) 함수

/**
  * @brief  This function is the implementation for tcp_recv LwIP callback
  * @param  arg: pointer on a argument for the tcp_pcb connection
  * @param  tpcb: pointer on the tcp_pcb connection
  * @param  pbuf: pointer on the received pbuf
  * @param  err: error information regarding the reveived pbuf
  * @retval err_t: error code
  */
static err_t tcp_echoserver_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
  struct tcp_echoserver_struct *es;
  err_t ret_err;

  LWIP_ASSERT("arg != NULL",arg != NULL);
  
  es = (struct tcp_echoserver_struct *)arg;
  
  // 수신한 tcp 프레임이 비어있는 경우 클라이언트와 커넥션을 끊는다.
  /* if we receive an empty tcp frame from client => close connection */
  if (p == NULL)
  {
  // CLOSING 상태로 변경
    /* remote host closed connection */
    es->state = ES_CLOSING;
    if(es->p == NULL)
    {
    // 처리할 데이터가 없다면 바로 연결을 끊고
       /* we're done sending, close connection */
       tcp_echoserver_connection_close(tpcb, es);
    }
    else
    {
    // 처리할 데이터가 남았다면 
      /* we're not done yet */
      // 송신완료에 대한 콜백함수를 지정하고
      /* acknowledge received packet */
      tcp_sent(tpcb, tcp_echoserver_sent);
      
      // 남은 데이터를 전송한다. 위의 송신 콜백함수는 송신 처리 후에 remote에서 Ack되고 호출된다.
      /* send remaining data*/
      tcp_echoserver_send(tpcb, es);
    }
    ret_err = ERR_OK;
  }   
  // 에러가 발생한 경우
  /* else : a non empty frame was received from client but for some reason err != ERR_OK */
  else if(err != ERR_OK)
  {
    /* free received pbuf*/
    if (p != NULL)
    {
      es->p = NULL;
      pbuf_free(p);
    }
    ret_err = err;
  }
  // 최초로 데이터를 수신한 경우에만 이 분기에 들어온다.
  else if(es->state == ES_ACCEPTED)
  {
  // 상태를 RECEIVED 로 변경
    /* first data chunk in p->payload */
    es->state = ES_RECEIVED;
    
    // 파라미터로 들어온 데이터 pbuf 주소를 es 구조체에 저장해 사용
    /* store reference to incoming pbuf (chain) */
    es->p = p;
    
    // 송신완료에 대한 콜백함수를 지정하고
    /* initialize LwIP tcp_sent callback function */
    tcp_sent(tpcb, tcp_echoserver_sent);
    
    // 수신 데이터를 그대로 전송한다.
    /* send back the received data (echo) */
    tcp_echoserver_send(tpcb, es);
    
    ret_err = ERR_OK;
  }
  // 최초 이후로 들어오는 데이터는 이 분기로 들어온다.
  else if (es->state == ES_RECEIVED)
  {
  // 클라이언트로 부터 새로운 데이터가 들어왔는데 전송은 모두 완료되어 버퍼가 비어있는 경우
    /* more data received from client and previous data has been already sent*/
    if(es->p == NULL)
    {
    // 새로 들어온 pbuf 주소를 es에 저장한다.
      es->p = p;
  	// 데이터 전송
      /* send back received data */
      tcp_echoserver_send(tpcb, es);
    }
    // 전송할 데이터가 아직 남아있는 상태면
    else
    {
      struct pbuf *ptr;
	
    // 새로 들어온 pbuf를 이전 데이터 버퍼 뒤에 연결한다.
      /* chain pbufs to the end of what we recved previously  */
      ptr = es->p;
      pbuf_chain(ptr,p);
    }
    ret_err = ERR_OK;
  }
  else if(es->state == ES_CLOSING)
  {
  // 연결 종료 중에 수신이 발생한 경우 데이터를 버리고 버퍼를 비운다.
    /* odd case, remote side closing twice, trash data */
    tcp_recved(tpcb, p->tot_len);
    es->p = NULL;
    pbuf_free(p);
    ret_err = ERR_OK;
  }
  else
  {
  // 정의되지 않는 상태인 경우 버퍼를 비운다.
    /* unknown es->state, trash data  */
    tcp_recved(tpcb, p->tot_len);
    es->p = NULL;
    pbuf_free(p);
    ret_err = ERR_OK;
  }
  return ret_err;
}

 

tcp client에서 보낸 걸  stm32가 echo 송신하는 부분은 위 함수 내에서 tcp_echoserver_send(tpcb, es);가 호출되는 두 군데이다.

 

 

 

 

 

 

 

 

 

static void tcp_echoserver_send(struct tcp_pcb *tpcb, struct tcp_echoserver_struct *es) 함수

/**
  * @brief  This function is used to send data for tcp connection
  * @param  tpcb: pointer on the tcp_pcb connection
  * @param  es: pointer on echo_state structure
  * @retval None
  */
static void tcp_echoserver_send(struct tcp_pcb *tpcb, struct tcp_echoserver_struct *es)
{
  struct pbuf *ptr;
  err_t wr_err = ERR_OK;
 
  while ((wr_err == ERR_OK) &&
         (es->p != NULL) && 
         (es->p->len <= tcp_sndbuf(tpcb)))
  {
    
    // es 구조체의 pbuf 포인터를 가져옴
    /* get pointer on pbuf from es structure */
    ptr = es->p;

	// 전송 큐에 payload(데이터)와 길이 전달
    /* enqueue data for transmission */
    wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);
    
    if (wr_err == ERR_OK)
    {
      u16_t plen;
      u8_t freed;

      plen = ptr->len;
     
     // 체인(링크드리스트)의 다음 pbuf를 p에 대입
      /* continue with next pbuf in chain (if any) */
      es->p = ptr->next;
      
      if(es->p != NULL)
      {
      // NULL인 경우 pbuf의 레퍼런스 카운트 증가
        /* increment reference count for es->p */
        pbuf_ref(es->p);
      }
      
      // 전송한 pbuf를 체인에서 떼어내고 free함
     /* chop first pbuf from chain */
      do
      {
        /* try hard to free pbuf */
        freed = pbuf_free(ptr);
      }
      while(freed == 0);
      // windows 사이즈를 늘리기 위해 수신 데이터 처리 후 호출되는 함수
     /* we can read more data now */
     tcp_recved(tpcb, plen);
   }
   else if(wr_err == ERR_MEM)
   {
      /* we are low on memory, try later / harder, defer to poll */
     es->p = ptr;
   }
   else
   {
     /* other problem ?? */
   }
  }
}

tcp_write()가 실제 전송하는 함수이다.

 

 

참고 : https://m.blog.naver.com/eziya76/220948522345

 

 

 

 

 

 

 

 

콜백  테스트

 

 

 

 

 

 

 

TCP 속도 테스트

 

 

 

 

 

 

 

 

단순 echo가 아니라 송신 데이터를 임의로 만들어서 보내기 위해서는 tcp_echoserver_send 함수 내부를 수정하면 될 것같다.

다만 여기서 주의할 것이 있다.

wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);

송신에 tcp_write() 함수를 사용하는 것은 맞는데 tcp echo server 예제에서 사용하듯이 ptr->payload 와 ptr->len에 내가 원하는 임의의 데이터를 대신 집어넣는 방식은 사용하면 안된다.

 

 

 

예제의 tcp_echoserver_send 함수는 이름에 걸맞지 않게 송신역할만 하는게 아니기 때문이다.

내부를 살펴보면 수신된 pbuf를 free하고 flow control에 사용되는 window 제어와 관련된 tcp_recved(tpcb, plen); 함수를 호출하는 것을 볼 수 있다.

tcp_recved() 함수는 데이터 처리 후에 pbuf를 free하고 더 많은 데이터를 받을 수 있도록 상대에게 window size를 알려주는 역할을 한다.

그런데 파라미터인 len이 수신된 값이 아니라 다른 값으로 덮어지면 window 제어에 문제가 생긴다.

 

 

tcp_recved() 함수 설명

/**
 * @ingroup tcp_raw
 * This function should be called by the application when it has
 * processed the data. The purpose is to advertise a larger window
 * when the data has been processed.
 *
 * @param pcb the tcp_pcb for which data is read
 * @param len the amount of bytes that have been read by the application
 */
void
tcp_recved(struct tcp_pcb *pcb, u16_t len)

 

 

 

참고.

TCP Window size 에 대한 추가 내용

https://accedian.com/blog/tcp-receive-window-everything-need-know/

https://noodles8436.tistory.com/11

 

[0x432-0x433] [보충] TCP HEADER Window Size

[0x433] TCP HEADER Window Size - Window Size 필드는 송신자에게 응답( Ack )을 보내기 전에, 얼마나 많은 데이터를 수신자가 더 받을 수 있는지 알려줍니다. - 만약 수신자가 데이터 ( =패킷 ) 를 받는대로 빠

noodles8436.tistory.com

 

 

 

 

 

그래서 아래처럼 tcp_echoserver_send() 함수 내부를 수정하는 방법이 있을 것 같다.

server 가 client 로부터 데이터를 수신하면 문자열을 전송한다.

static void tcp_echoserver_send(struct tcp_pcb *tpcb, struct tcp_echoserver_struct *es)
{
  struct pbuf *ptr;
  err_t wr_err = ERR_OK;
 
  while ((wr_err == ERR_OK) &&
         (es->p != NULL) &&
         (es->p->len <= tcp_sndbuf(tpcb)))
  {
    
    /* get pointer on pbuf from es structure */
    ptr = es->p;

    /* enqueue data for transmission */
    wr_err = tcp_write(tpcb, "\nhi\n", 5, 1);

    if (wr_err == ERR_OK)
    {
      u16_t plen;
      u8_t freed;

      plen = ptr->len;
     
      /* continue with next pbuf in chain (if any) */
      es->p = ptr->next;
      
      if(es->p != NULL)
      {
        /* increment reference count for es->p */
        pbuf_ref(es->p);
      }
      
     /* chop first pbuf from chain */
      do
      {
        /* try hard to free pbuf */
        freed = pbuf_free(ptr);
      }
      while(freed == 0);


     /* we can read more data now */
     tcp_recved(tpcb, plen);
   }
   else if(wr_err == ERR_MEM)
   {
       //we are low on memory, try later / harder, defer to poll
     es->p = ptr;
   }
   else
   {
      //other problem ??
   }
  }
}

 

 

 

 

 

 

 

 

 

 

client 정보와 수신 데이터를 활용하는 버전

 

static void tcp_echoserver_send(struct tcp_pcb *tpcb, struct tcp_echoserver_struct *es)
{
  struct pbuf *ptr;
  err_t wr_err = ERR_OK;
 
  while ((wr_err == ERR_OK) &&
         (es->p != NULL) &&
         (es->p->len <= tcp_sndbuf(tpcb)))
  {
    
    /* get pointer on pbuf from es structure */
    ptr = es->p;

	char buf[100];
	memset (buf, '\0', 100);

	/* get the Remote IP */
	ip4_addr_t inIP = tpcb->remote_ip;

	/* Extract the IP */
	char *remIP = ipaddr_ntoa(&inIP);

	strncpy(buf, (char *)es->p->payload, es->p->tot_len);
	strcat(buf, "\nYour IP : ");
	strcat(buf, remIP);
	strcat(buf, "\n");
	strcat(buf, "Hello from TCP SERVER\n");

    /* enqueue data for transmission */
    wr_err = tcp_write(tpcb, buf, strlen(buf)+1, 1);


    if (wr_err == ERR_OK)
    {
      u16_t plen;
      u8_t freed;

      plen = ptr->len;
     
      /* continue with next pbuf in chain (if any) */
      es->p = ptr->next;
      
      if(es->p != NULL)
      {
        /* increment reference count for es->p */
        pbuf_ref(es->p);
      }
      
     /* chop first pbuf from chain */
      do
      {
        /* try hard to free pbuf */
        freed = pbuf_free(ptr);
      }
      while(freed == 0);


     /* we can read more data now */
     tcp_recved(tpcb, plen);
   }
   else if(wr_err == ERR_MEM)
   {
       //we are low on memory, try later / harder, defer to poll
     es->p = ptr;
   }
   else
   {
      //other problem ??
   }
  }
}

 

 

 

 

 

 

참고.

Middlewares - Third_Party - LWIP - src - core 를 살펴보면 주석을 통해 함수의 정보를 얻을 수 있다.

 * Sending TCP data
 * ----------------
 * TCP data is sent by enqueueing the data with a call to tcp_write() and
 * triggering to send by calling tcp_output(). When the data is successfully
 * transmitted to the remote host, the application will be notified with a
 * call to a specified callback function.
 * - tcp_write()
 * - tcp_output()
 * - tcp_sent()
 * 
 * Receiving TCP data
 * ------------------
 * TCP data reception is callback based - an application specified
 * callback function is called when new data arrives. When the
 * application has taken the data, it has to call the tcp_recved()
 * function to indicate that TCP can advertise increase the receive
 * window.
 * - tcp_recv()
 * - tcp_recved()

 

 

 

 

 

 

 

+ 클라이언트에 대한 응답이 아니라 서버가 tcp_write()를 사용해 클라이언트에게 데이터를 보내는 법

 

질문이 나와서 추가해 봅니다. 기존 코드를 최소한으로 수정하면서 테스트해보았습니다.

 

먼저 recv callback 함수가 아니라 다른 곳에서 클라이언트에게 데이터를 전송하려면 tcp connection이 이뤄진 pcb정보를 전역으로 알고있을 필요가 있습니다.

 

따라서 다음과 같이 코드를 추가합니다.

 

tcp_echoserver.c

// ...
// accept된 pcb 주소를 저장할 pcb 구조체 포인터
struct tcp_pcb *tcp_pcb;
// ...
static err_t tcp_echoserver_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
// ...
  if (es != NULL)
  {
// ...
	// accept된 경우 tcp connection이 이뤄진 클라이언트 정보가 담긴 pcb 주소를 tcp_pcb에 대입
    tcp_pcb = newpcb;
  }
  else
  {
// ...
  }
//...
}
static void tcp_echoserver_connection_close(struct tcp_pcb *tpcb, struct tcp_echoserver_struct *es)
{
// ...
  // connection이closed된 경우는 NULL포인터 대입 
  tcp_pcb = NULL;
// ...
}

 

 

main.c

// ...
/* USER CODE BEGIN Includes */
#include <tcp_echoserver.h>
#include "lwip/tcp.h"
#include "lwip/err.h"
#include <string.h>
/* USER CODE END Includes */
// ...
/* USER CODE BEGIN PV */
extern struct netif gnetif;
// tcp_pcb extern 선언
extern struct tcp_pcb *tcp_pcb;
/* USER CODE END PV */
// ...
int main(void)
{
// ...
  /* USER CODE BEGIN 2 */
  tcp_echoserver_init();
  uint32_t memTick = 0;
  err_t err;
  /* USER CODE END 2 */  
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  	ethernetif_input(&gnetif);
  	sys_check_timeouts();
    
    // tcp_pcb가 NULL이 아닌경우, 즉 연결된 클라이언트 정보가 있는 경우 1초간격으로 메시지 전송
  	if(HAL_GetTick() - memTick >= 1000 && tcp_pcb != NULL)
  	{
  		memTick = HAL_GetTick();
  		err = tcp_write(tcp_pcb, "This is sent from main loop\n", strlen("This is sent from main loop\n"), 1);
  	}
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

 

 

 

 

 

 

 

다만 TCP서버는 여러 클라이언트를 listen하고 accept할 수 있는데 위 코드는 마지막으로 accept된 클라이언트에게만 1초 주기로 메시지를 보내고 접속된 클라이언트 중 하나라도 연결이 끊이면 메시지도 끊기는 겁니다.

 

그러니 실사용할때는 예외처리나 추가적인 관리코드가 필요합니다.

 

예를들면 lwip/opt.h에서 listen하는 tcp pcb num을 제한 걸수도 있고 아니면 tcp_pcb를 접속가능한 클라이언트 수 설정만큼 array로 만들고 순환구조로 관리하는 방법도 있을것 같습니다.