天天看點

LWIP應用開發|并發伺服器

并發伺服器

1. 基于多線程的并發伺服器

并發伺服器支援多個用戶端的連接配接,最大可接入的用戶端數取決于核心控制塊的個數。當使用Socket API時,要使伺服器能夠同時支援多個用戶端的連接配接,必須引入多任務機制,為每個連接配接建立一個單獨的任務來處理連接配接上的資料,我們将這個設計方式稱作并發伺服器的設計。

由于多線程并發伺服器涉及到子任務的動态建立和銷毀,使用者需要自己完成對任務堆棧的管理和回收,是以并發伺服器的設計流程也相對複雜。

以下并發伺服器執行個體完成的功能為:伺服器能夠同時支援多個用戶端的連接配接,并能夠将每個連接配接上接收到的小寫字母轉換成大寫字母回顯到用戶端,其實作步驟如下

  • 參考Socket API程式設計優化一文,在該文的工程源碼基礎上進行修改
  • 在工程中建立socket_thread_server.c和對應的頭檔案
/******socket_thread_server.c******/
#include "socket_tcp_server.h"
#include "socket_wrap.h"
#include "FreeRTOS.h"
#include "task.h"
#include "cmsis_os.h"
#include "ctype.h"

static char ReadBuff[BUFF_SIZE];
/**
  * @brief  Function implementing the defaultTask thread.
  * @param  argument: Not used 
  * @retval None
  */
/* USER CODE END Header_StartDefaultTask */
void vNewClientTask(void const * argument){
  // 每一個任務,都有獨立的棧空間
  int cfd = * (int *)argument;
  int n, i;
  while(1){
	//等待用戶端發送資料
	n = Read(cfd, ReadBuff, BUFF_SIZE);
	if(n <= 0){
	  close(cfd);
	  vTaskDelete(NULL);
	}
	//進行大小寫轉換
	for(i = 0; i < n; i++){	
      ReadBuff[i] = toupper(ReadBuff[i]);		
	}
	//寫回用戶端
	n = Write(cfd, ReadBuff, n);
	if(n < 0){
	  close(cfd);
	  vTaskDelete(NULL);			
	}
  }
}
/**
  * @brief  多線程伺服器
  * @param  none
  * @retval none
  */
void vThreadServerTask(void){
  int sfd, cfd;
  struct sockaddr_in server_addr, client_addr;
  socklen_t	client_addr_len;
  //建立socket
  sfd = Socket(AF_INET, SOCK_STREAM, 0);
  server_addr.sin_family 			= AF_INET;
  server_addr.sin_port   			= htons(SERVER_PORT);
  server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  //綁定socket
  Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
  //監聽socket
  Listen(sfd, 5);
  //等待用戶端連接配接
  client_addr_len = sizeof(client_addr);
  while(1){
	/*每建立一個socket,lwip都會配置設定一片記憶體空間
	宏NUM_SOCKETS就定義了一共支援多少個socket,即能配置設定多少fd
	#define NUM_SOCKETS			MEMP_NUM_NETCONN
	#define MEMP_NUM_NETCONN	8		
	*/
	cfd = Accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len);
	printf("client is connect cfd = %d\r\n",cfd);
	if(xTaskCreate( (TaskFunction_t) vNewClientTask,
									"Client",
									128,//1k
									(void *)&cfd,
									osPriorityNormal,
									NULL) != pdPASS){	
		printf("create task fail!\r\n");		
	}
  }									
}
           
  • 在freertos.c檔案中的預設任務裡面添加代碼
void StartDefaultTask(void const * argument){
  /* init code for LWIP */
  MX_LWIP_Init();
  /* USER CODE BEGIN StartDefaultTask */
  printf("TCP thread server started!\r\n",cfd);
  /* Infinite loop */
  for(;;){
    vThreadServerTask();
	osDelay(100);
  }
  /* USER CODE END StartDefaultTask */
}
           
  • 編譯無誤下載下傳到開發闆後,打開序列槽助手可以看到相關調試資訊,使用網絡調試工具可以建立多個PC用戶端(序列槽會傳回對應的cfd),輸入任意小寫字母,Server将傳回對應的大寫字母
LWIP應用開發|并發伺服器
LWIP應用開發|并發伺服器

2. 基于Select的并發伺服器

基于多線程的socket并發伺服器,必須使用多線程的方式來實作,即為每個連接配接建立一個單獨的任務來處理資料。但是,這種多線程的方式是有缺陷的,在大型伺服器的設計中,一個伺服器上可能存在成千上萬條連接配接,如果為每個連接配接都建立一個線程,這對系統資源來說無疑是比巨大的開銷,也是種不太現實的做法。事實上,在socket程式設計中,通常使用一種叫做Select的機制來實作并發伺服器的設計。

Select函數實作的基本思想為:先構造一張有關描述符的表,然後調用一個函數。當這些檔案描述符中的一個或多個已準備好進行I/O時函數才傳回;函數傳回時告訴程序哪個描述符已就緒,可以進行I/O操作

/*****select()函數*****/
函數原型:int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
傳 入 值:maxfd 監控的檔案描述符集裡最大檔案描述符加1
		 readfds 監控有讀資料到達檔案描述符集合,傳入傳出參數
		 writefds 監控有寫資料到達檔案描述符集合,傳入傳出參數
		 exceptfds 監控異常發生達檔案描述符集合,傳入傳出參數
		 timeout 逾時設定 
		 -->NULL:一直阻塞,直到有檔案描述符就緒或出錯
		 -->0:僅僅檢測檔案描述符集的狀态,然後立即傳回,輪詢
		 -->不為0:在指定時間内,如果沒有事件發生,則逾時傳回
返 回 值:成功:所監聽的所有監聽集合中,滿足條件的總數!
		 失敗:0 逾時
		 錯誤:-1
//timeval結構體
struct timeval {
	long tv_sec; /* seconds */
	long tv_usec; /* microseconds */
};
           

調用 select() 函數時程序會一直阻塞直到有檔案可讀、有檔案可寫或者逾時時間到。為了設定檔案描述符需要使用幾個宏:

注意:

1. select能監聽的檔案描述符個數受限于FD_SETSIZE,一般為1024,單純改變程序打開的檔案描述符個數并不能改變select監聽檔案個數

2. 解決1024以下用戶端時使用select是很合适的,但如果連結用戶端過多,select采用的是輪詢模型,會大大降低伺服器響應效率,不應在select上投入更多精力

#include <sys/select.h>
int FD_ZERO(fd_set *fdset);			//從fdset中清除所有的檔案描述符
int FD_CLR(int fd,fd_set *fdset);	//将fd從fdset中清除
int FD_SET(int fd,fd_set *fdset);	//将fd加入到fdset
int FD_ISSET(int fd,fd_set *fdset);	//判斷fd是否在fdset集合中
/*例如*/
fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(fd,&rset);
FD_SET(stdin,&rset);
//在select傳回之後,可以使用FD_ISSET(fd,&rset)測試給定的位置是否置位。
if(FD_ISSET(fd,&rset))
{......}
           

select程式設計模型如下圖示

LWIP應用開發|并發伺服器
LWIP應用開發|并發伺服器

以下并發伺服器執行個體完成的功能為:伺服器能夠同時支援多個用戶端的連接配接,并能夠将每個連接配接上接收到的小寫字母轉換成大寫字母回顯到用戶端,其實作步驟如下:

  • 參考Socket API程式設計優化一文,在該文的工程源碼基礎上進行修改
#include "socket_wrap.h"
#include "socket_select_server.h"
#include "socket_tcp_server.h"
#include "string.h"
#include "FreeRTOS.h"
#include "task.h"
#include "ctype.h"

static char ReadBuff[BUFF_SIZE];
/**
  * @brief  select 并發伺服器
  * @param  none
  * @retval none
  */
void vSelectServerTask(void){
  int sfd, cfd, maxfd, i, nready, n, j;
  struct sockaddr_in server_addr, client_addr;
  socklen_t	client_addr_len;
  fd_set all_set, read_set;
  //FD_SETSIZE裡面包含了伺服器的fd
  int clientfds[FD_SETSIZE - 1];	
  //建立socket
  sfd = Socket(AF_INET, SOCK_STREAM, 0);
  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons(SERVER_PORT);
  server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  //綁定socket
  Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
  //監聽socket
  Listen(sfd, 5);	
  client_addr_len = sizeof(client_addr);
  //初始化 maxfd 等于 sfd
  maxfd = sfd;	
  //清空fdset
  FD_ZERO(&all_set);	
  //把sfd檔案描述符添加到集合中	
  FD_SET(sfd, &all_set);
  //初始化用戶端fd的集合
  for(i = 0; i < FD_SETSIZE -1 ; i++){
	//初始化為-1
	clientfds[i] = -1;
  }
  while(1){
	//每次select傳回之後,fd_set集合就會變化,再select時,就不能使用,
	//是以我們要儲存設定fd_set 和 讀取的fd_set
	read_set = all_set;
	nready = select(maxfd + 1, &read_set, NULL, NULL, NULL);
	//沒有逾時機制,不會傳回0
	if(nready < 0){
	  printf("select error \r\n");
	  vTaskDelete(NULL);
	}
	//判斷監聽的套接字是否有資料
	if(FD_ISSET(sfd, &read_set)){	
	  //有用戶端進行連接配接了
	  cfd = accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len);
	  if(cfd < 0){
		printf("accept socket error\r\n");
		//繼續select
		continue;
	  }
	  printf("new client connect fd = %d\r\n", cfd);
	  //把新的cfd 添加到fd_set集合中
	  FD_SET(cfd, &all_set);
	  //更新要select的maxfd
	  maxfd = (cfd > maxfd)?cfd:maxfd;
	  //把新的cfd 儲存到cfds集合中
	  for(i = 0; i < FD_SETSIZE -1 ; i++){
		if(clientfds[i] == -1){
		  clientfds[i] = cfd;
		  //退出,不需要添加
		  break;		
		}
	  }
	  //沒有其他套接字需要處理:這裡防止重複工作,就不去執行其他任務
	  if(--nready == 0){
		//繼續select
		continue;
	  }	
    }
	//周遊所有的用戶端檔案描述符
	for(i = 0; i < FD_SETSIZE -1 ; i++){
	  if(clientfds[i] == -1){
		//繼續周遊
		continue;
	  }
	  //是否在我們fd_set集合裡面
	  if(FD_ISSET(clientfds[i], &read_set)){
		n = Read(clientfds[i], ReadBuff, BUFF_SIZE);
		//Read函數已經關閉了這個用戶端的fd
		if(n <= 0){
		  //從集合裡面清除
		  FD_CLR(clientfds[i], &all_set);
		  //目前的用戶端fd 指派為-1
		  clientfds[i] = -1;
		}else{
		  //進行大小寫轉換
		  for(j = 0; j < n; j++){		
			ReadBuff[j] = toupper(ReadBuff[j]);		
		  }
		  //寫回用戶端
		  n = Write(clientfds[i], ReadBuff, n);
		  if(n < 0){
			//從集合裡面清除
			FD_CLR(clientfds[i], &all_set);
			//目前的用戶端fd 指派為-1
			clientfds[i] = -1;		
		  }				
		}
	  }
	}		
  }
}

           
  • 在freertos.c檔案中的預設任務裡面添加代碼
void StartDefaultTask(void const * argument){
  /* init code for LWIP */
  MX_LWIP_Init();
  /* USER CODE BEGIN StartDefaultTask */
  printf("TCP thread server started!\r\n",cfd);
  /* Infinite loop */
  for(;;){
    vSocketServerTask();
	osDelay(100);
  }
  /* USER CODE END StartDefaultTask */
}
           
  • 編譯無誤下載下傳到開發闆後,打開序列槽助手可以看到相關調試資訊,使用網絡調試工具可以建立多個PC用戶端(序列槽會傳回對應的cfd),輸入任意小寫字母,Server将傳回對應的大寫字母

繼續閱讀