UDP伺服器併發思路
阿新 • • 發佈:2020-12-07
UDP併發與TCP併發的區別
在TCP併發程式設計中,通常使用one loop per thread的併發模型,也就是使用多個執行緒,每個執行緒中都有一個epoll loop,無論是使用epoll還是poll或select,在觀察有無資料就緒時,都是針對多個檔案描述符。如果只有一個檔案描述符,那麼程序只要觀察那一個檔案描述符即可。
網路程式設計中,一個Socket對應一個檔案描述符。在TCP的併發中,伺服器在監聽埠初始化一個socket套接字描述符,接受客戶端後就與每個客戶端的連線有一個不同的檔案描述符,所以TCP併發中有多個socket套接字描述符。但是,UDP協議的伺服器沒有真正意義上的“連線”的概念。在訊息監聽埠和響應請求都只有一個socket套接字描述符。
UDP怎麼考慮併發?
《UNP · 卷1》中UDP章節描述了一句話:一般來說大多數TCP伺服器是併發的,而大多數UDP伺服器是迭代的。也就是伺服器等待客戶端的請求,然後讀取請求後處理,再發迴響應。如果是簡單的處理響應還可以,如果每個處理都很耗時,那麼就不得不考慮在UDP伺服器做併發處理。
併發常見的思路就是多執行緒。伺服器讀取一個新的請求後,可以交給一個執行緒處理,該執行緒在處理之後直接將響應內容發給客戶端。
雖然可以多執行緒處理讀取的每個訊息,但如果UDP伺服器與多個客戶端互動,卻沒有多個socket,這樣效率並不是很高。典型的解決方法就是:在伺服器為每個客戶端建立一個新的socket套接字並繫結一個新的埠,客戶端以後就需要以這個新的socket套接字與伺服器通訊。
總的來說:UDP併發伺服器針對多個客戶端,可以建立多個socket。針對多個請求,可以使用多執行緒(執行緒池)進行處理。
UDP 併發程式設計模型
- 多個socket(虛擬碼)
for (; ;) { //等待新的客戶端連線 recvfrom(&from_addr); //每有一個新的客戶端,建立一個執行緒 pthread_create(&tid, NULL, thread_fun, &from_addr); } //執行緒函式 void* thread_fun(void* arg) { peer = socket(AF_INET, SOCK_DGRAM, 0); servaddr.sin_port = htons(0); //繫結埠 bind(peer, (struct sockaddr*)&servaddr, sizeof(servaddr)); //將這個套接字和客戶端地址連線,之後就可以使用write/read或send/recv這些函式,且不用再關心客戶端地址 connect(peer, (struct sockaddr*)&from, sizeof(from)); //處理請求的loop }
- 使用epoll進行處理(虛擬碼)
- UDP伺服器建立socket,並設定socket為
REUSEADDR
、REUSEPORT
和非阻塞同時再bind伺服器地址local_addr:
listen_fd = socket(PF_INET, SOCK_DGRAM, 0);
fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFD, 0)|O_NONBLOCK)
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
bind(listen_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
- 建立epoll fd,並將listen_fd新增到epoll中,並監聽其可讀事件:
epoll_fd = epoll_create(100);
ep_event.events = EPOLLIN | EPOLLET;
ep_event.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ep_event);
while (1)
{
in_fds = epoll_wait(epoll_fd, in_events, 1000, 5000);
- epoll_wait返回時,如果返回的是listen_fd, 呼叫
recvfrom
接受client第一個UDP包,並根據recvfrom返回client地址,建立一個新的socket套接字new_fd,設定new_fd為REUSEADDR
、REUSEPORT
和非阻塞,同時bind本地地址local_addr然後connect上recvfrom返回的client地址:
for (i = 0; i < in_fds; i++)
{
if(in_events[i].data.fd = listen_fd)
{
recvfrom(listen_fd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &client_len);
new_fd = socket(PF_INET, SOCK_DGRAM, 0);
fcntl(new_fd, F_SETFL, fcntl(new_fd, F_GETFD, 0)|O_NONBLOCK);
setsockopt(new_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(new_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
bind(new_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
connect(new_fd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr));
- 將新建立的new_fd加入到epoll中並監聽其可讀事件:
client_ev.events = EPOLLIN;
client_ev.data.fd = new_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &client_ev);
}
else if (in_events[i].events & EPOLLIN)
{
- 當epoll_wait返回時,如果返回的是new_fd,那麼呼叫
recvfrom
來接收特定client的UDP包:
recvfrom(new_fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&client_addr, &client_len);
data->fd = new_fd;
data-> ptr= process(recvbuf); /*data中包括socket資訊*/
ev.data.ptr = data;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epoll_fd,EPOLL_CTL_MOD,new_fd,&ev);
}
else if (in_events[i].events & EPOLLOUT)
{
sockfd = data->fd;
send( sockfd, data->ptr, strlen((char*)data->ptr), 0 );
ev.data.ptr = data;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epoll_fd,EPOLL_CTL_MOD,sockfd,&ev);
}
else
{
}
}
}