一、前言
手把手教你從0開始編寫TCP服務(wù)器程序,體驗開局一塊磚,大廈全靠壘。
為了避免篇幅過長使讀者感到乏味,對【TCP服務(wù)器的開發(fā)】進行分階段實現(xiàn),一步步進行優(yōu)化升級。
本節(jié),在上一章節(jié)的基礎(chǔ)上,將并發(fā)的實現(xiàn)改為IO多路復(fù)用機制,使用select管理每個新接入的客戶端連接,實現(xiàn)發(fā)送和接收。
二、新增使用API函數(shù)
2.1、select()函數(shù)
函數(shù)原型:
#include <sys/types.h>
#include <unistd.h>
int select(int maxfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select函數(shù)共有5個參數(shù),其中參數(shù):
- maxfds:監(jiān)視對象文件描述符數(shù)量。
- readset:將所有關(guān)注“是否存在待讀取數(shù)據(jù)”的文件描述符注冊到fd_set變量,并傳遞其地址值。
- writeset: 將所有關(guān)注“是否可傳輸無阻塞數(shù)據(jù)”的文件描述符注冊到fd_set變量,并傳遞其地址值。
- exceptset:將所有關(guān)注“是否發(fā)生異常”的文件描述符注冊到fd_set變量,并傳遞其地址值。
- timeout:調(diào)用select后,為防止陷入無限阻塞狀態(tài),傳遞超時信息。
返回值:
- 錯誤返回-1。
- 超時返回0。
當(dāng)關(guān)注的事件返回時,返回大于0的值,該值是發(fā)生事件的文件描述符數(shù)。
2.2、FD_*系列函數(shù)
函數(shù)原型:
#include <sys/types.h>
#include <unistd.h>
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
(1)FD_CLR函數(shù)用于將fd從set集合中清除,即不監(jiān)控該fd的事件。
(2)FD_SET函數(shù)用于將fd添加到set集合中,監(jiān)控其事件。
(3)FD_ZERO函數(shù)用于將set集合重置。
(4)FD_ISSET函數(shù)用于判斷set集合中的fd是否有事件(讀、寫、錯誤)。
三、實現(xiàn)步驟
什么是IO多路復(fù)用?通俗的講就是一個線程,通過記錄IO流的狀態(tài)來管理多個IO。解決創(chuàng)建多個進程處理IO流導(dǎo)致CPU占用率高的問題。
select是io多路復(fù)用的一種方式,其他的還有poll、epoll等。
(1)創(chuàng)建socket。
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
(2)綁定地址。
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
(3)設(shè)置監(jiān)聽。
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
(4)初始化可讀文件描述符集合,將監(jiān)聽套接字加入集合。
fd_set writefds,readfds,wset,rset;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(listenfd,&readfds);
(5)從可讀文件描述符集合中選擇一個就緒的套接字。
wset=writefds;
rset=readfds;
// 從可讀文件描述符集合中選擇就緒的套接字
int nready=select(maxfd+1,&rset,&wset,NULL,NULL);
if(nready==-1)
{
printf("select errno = %d, %s\n",errno,strerror(errno));
continue;
}
(6)如果監(jiān)聽套接字有新連接請求,處理新連接。
struct sockaddr_in client;
memset(&client,0,sizeof(client));
socklen_t len=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if(clientfd==-1){
printf("accept errno = %d, %s\n",errno,strerror(errno));
}
else{
printf("accept successdul, clientfd = %d\n",clientfd);
// 將新套接字加入可讀文件描述符集合
FD_SET(clientfd,&readfds);
if(clientfd>maxfd)
maxfd=clientfd;
}
(7)處理客戶端發(fā)來的數(shù)據(jù)和發(fā)送數(shù)據(jù)到客戶端。
int i=0;
for(i=listenfd+1;i<=maxfd;i++)
{
if(FD_ISSET(i,&rset))
{
printf("recv fd=%d\n",i);
ret=recv(i,buf,BUFFER_LENGTH,0);
if(ret==0) {
// 客戶端斷開連接
printf("connection dropped\n");
// 從可讀文件描述符集合中移除該套接字
FD_CLR(i,&readfds);
close(i);
}
else if(ret>0)
{
printf("fd=%d recv --> %s\n",i,buf);
FD_CLR(i,&readfds);
FD_SET(i,&writefds);
}
}
else if(FD_ISSET(i,&wset))
{
printf("send to fd=%d\n",i);
ret=send(i,buf,ret,0);
if(ret==-1)
{
printf("send() errno = %d, %s\n",errno,strerror(errno));
}
FD_CLR(i,&writefds);
FD_SET(i,&readfds);
}
}
四、完整代碼
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#define LISTEN_PORT 9999
#define BLOCK_SIZE 10
#define BUFFER_LENGTH 1024
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_BIND_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4,
SOCKET_SELECT_FAILED=-5
};
int main(int argc,char **argv)
{
// 1.
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
// 2.
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
// 3.
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
printf("listen port: %d\n",LISTEN_PORT);
fd_set writefds,readfds,wset,rset;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(listenfd,&readfds);
char buf[BUFFER_LENGTH]={0};
int ret=0;
int maxfd=listenfd;
while(1)
{
wset=writefds;
rset=readfds;
// 從可讀文件描述符集合中選擇就緒的套接字
int nready=select(maxfd+1,&rset,&wset,NULL,NULL);
if(nready==-1)
{
printf("select errno = %d, %s\n",errno,strerror(errno));
continue;
}
// 如果監(jiān)聽套接字有新連接請求,處理新連接
if(FD_ISSET(listenfd,&rset))
{
// 4.
printf("accept , listenfd = %d\n",listenfd);
struct sockaddr_in client;
memset(&client,0,sizeof(client));
socklen_t len=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if(clientfd==-1){
printf("accept errno = %d, %s\n",errno,strerror(errno));
}
else{
printf("accept successdul, clientfd = %d\n",clientfd);
// 將新套接字加入可讀文件描述符集合
FD_SET(clientfd,&readfds);
if(clientfd>maxfd)
maxfd=clientfd;
}
}
printf("listenfd=%d.maxfd=%d\n",listenfd,maxfd);
int i=0;
for(i=listenfd+1;i<=maxfd;i++)
{
if(FD_ISSET(i,&rset))
{
printf("recv fd=%d\n",i);
ret=recv(i,buf,BUFFER_LENGTH,0);
if(ret==0) {
// 客戶端斷開連接
printf("connection dropped\n");
// 從可讀文件描述符集合中移除該套接字
FD_CLR(i,&readfds);
close(i);
}
else if(ret>0)
{
printf("fd=%d recv --> %s\n",i,buf);
FD_CLR(i,&readfds);
FD_SET(i,&writefds);
}
}
else if(FD_ISSET(i,&wset))
{
printf("send to fd=%d\n",i);
ret=send(i,buf,ret,0);
if(ret==-1)
{
printf("send() errno = %d, %s\n",errno,strerror(errno));
}
FD_CLR(i,&writefds);
FD_SET(i,&readfds);
}
}
}
close(listenfd);
return 0;
}
編譯命令:
gcc -o server server.c
五、TCP客戶端
5.1、自己實現(xiàn)一個TCP客戶端
自己實現(xiàn)一個TCP客戶端連接TCP服務(wù)器的代碼:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_LENGTH 1024
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_CONN_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4
};
int main(int argc,char** argv)
{
if(argc<3)
{
printf("Please enter the server IP and port.");
return 0;
}
printf("connect to %s, port=%s\n",argv[1],argv[2]);
int connfd=socket(AF_INET,SOCK_STREAM,0);
if(connfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
struct sockaddr_in serv;
serv.sin_family=AF_INET;
serv.sin_addr.s_addr=inet_addr(argv[1]);
serv.sin_port=htons(atoi(argv[2]));
socklen_t len=sizeof(serv);
int rwfd=connect(connfd,(struct sockaddr*)&serv,len);
if(rwfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
close(rwfd);
return SOCKET_CONN_FAILED;
}
int ret=1;
while(ret>0)
{
char buf[BUFFER_LENGTH]={0};
printf("Please enter the string to send:\n");
scanf("%s",buf);
send(connfd,buf,strlen(buf),0);
memset(buf,0,BUFFER_LENGTH);
printf("recv:\n");
ret=recv(connfd,buf,BUFFER_LENGTH,0);
printf("%s\n",buf);
}
close(rwfd);
return 0;
}
編譯:
gcc -o client client.c
5.2、Windows下可以使用NetAssist的網(wǎng)絡(luò)助手工具
下載地址:http://old.tpyboard.com/downloads/NetAssist.exe
小結(jié)
至此,我們實現(xiàn)了一個使用IO多路復(fù)用機制實現(xiàn)的服務(wù)器,這時的TCP服務(wù)器可以使用一個線程就能處理多個客戶端連接。通過記錄IO流的狀態(tài)來管理多個IO,解決創(chuàng)建多個進程處理IO流導(dǎo)致CPU占用率高的問題。
我們總結(jié)一下select的使用流程:
1、定義io管理狀態(tài)變量:fd_set rfds,wfds;
2、初始化變量:FD_ZERO();
3、設(shè)置io流狀態(tài),最初只有監(jiān)聽的fd,將其設(shè)置:FD_SET(listenfd,rfds);
4、在循環(huán)中select。
5、FD_ISSET()判斷端口是否有連接。
6、FD_ISSET()判斷可讀、可寫狀態(tài)。文章來源:http://www.zghlxwxcb.cn/news/detail-773149.html
select是io多路復(fù)用的一種方式,其他的還有poll、epoll等。下一章節(jié)我們將使用更高效的IO多路復(fù)用器epoll來實現(xiàn)TCP服務(wù)器。文章來源地址http://www.zghlxwxcb.cn/news/detail-773149.html
到了這里,關(guān)于TCP服務(wù)器的演變過程:IO多路復(fù)用機制select實現(xiàn)TCP服務(wù)器的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!