并發服務器
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將返回對應的大寫字母
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() 函數時進程會一直阻塞直到有文件可讀、有文件可寫或者超時時間到。 為了設置文件描述符需要使用幾個宏:
- select能監聽的文件描述符個數受限于FD_SETSIZE,一般為1024,單純改變進程打開的文件描述符個數并不能改變select監聽文件個數
- 解決1024以下客戶端時使用select是很合適的,但如果鏈接客戶端過多,select采用的是輪詢模型,會大大降低服務器響應效率,不應在select上投入更多精力
#include
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編程模型如下圖示
以下并發服務器實例完成的功能為:服務器能夠同時支持多個客戶端的連接,并能夠將每個連接上接收到的小寫字母轉換成大寫字母回顯到客戶端,其實現步驟如下:
參考Socket API編程優化一文,在該文的工程源碼基礎上進行修改
在工程中創建socket_socket_server.c和對應的頭文件
#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將返回對應的大寫字母
-
服務器
+關注
關注
12文章
9029瀏覽量
85207 -
API
+關注
關注
2文章
1487瀏覽量
61833 -
調試
+關注
關注
7文章
574瀏覽量
33901 -
編程
+關注
關注
88文章
3596瀏覽量
93610 -
多線程
+關注
關注
0文章
277瀏覽量
19923
發布評論請先 登錄
相關推薦
評論