网络编程实验2-循环服务器设计与select多路转换
实验目的
本次实验的主要目的是学习设计循环服务器,并学习使用select实现单线程并发。
实验内容
1、设计基于UDP的循环服务器;
2、使用select设计客户端程序。
实验基础知识
1、UDP循环服务器
单个执行线程使用一个套接字与多个客户通信。
2、循环的、无连接的服务器的算法
创建套接字并将其绑定到所提供服务的熟知端口上;
重复地读取来自客户的请求,构造响应,按照应用协议向客户发回响应。
3、创建被动套接字
int passivesock(const char *service, const char *transport,int qlen );
int passiveUDP ( const *service );
{
return passivesock( service,” udp” , 0 );
}
4、服务器的并发
服务器只使用单个控制线程,就能为若干个客户提供表面上的并发性。
5、服务器中的数据驱动处理
若并发服务器处理每个请求仅需很少的时间,通常它就按顺序方式执行,而执行由数据的到达驱动。只有在工作量太大,以致CPU不能顺序执行时,分时机制才取而代之。
6、用单线程进行数据驱动处理
编写一个单线程并发服务器的关键是通过在操作系统原语select中使用异步I/0。一个服务器为它必须管理的每一个连接创建一个套接字,然后调用select等待任意连接上数据的到达。
实际上,由于select可在所有的套接字上等待I/O,它也能同时等待新的的连接。
7、select 函数
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefdsfd_set *errorfds, struct timeval *timeout);
struct timeval {
time_t tv_sec; /* 秒seconds */
long tv_usec; /* 微秒microseconds */
}
返回值:>0, 状态发生变化的描述符的总数;
=0, 超时;
=-1,出错。
其中:nfds: maxfds+1;
readfds: 读数据测试的fds;
wirtefds: 写数据测试的fds;
errorfds: 异常数据测试的fds;
timeout: 超时指针,NULL: 阻塞;
tv_sec = 0;
tv_usec = 0 : 非阻塞。
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 500000;
8、使用select多路转接
使用select多路转接实现IO多路转接,先构造一张有关描述符的列表,然后调用select,直到这些描述符中的一个已准备好进行IO时,该函数才返回。返回时,它告诉进程哪些描述符已准备好可进行IO。
使用多路转接所用API:
#include<sys/select.h>
int select(int maxfdp1,fd_set* rfds,fd_set* wfds,fd_set* efds,struct timeval* tvptr);
int FD_ISSET(int fd,fd_set* fdset);
void FD_CLR(int fd,fd_set* fdset);
void FD_SET(int fd,fd_set* fdset);
void FD_ZERO(fd_set* fdset);
实验步骤
1.编写头文件sockutil.h
#ifndef SOCKUTIL_H
#define SOCKUTIL_H
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<netdb.h>
#ifndef INADDR_NONE
#define INADDR_NONE 0xFFFFFFFF
#endif
int connectSock(char* host,char* service,char* protocol);
int passiveSock(char* service,char* protocol,int qlen);
void errexit(char* fmt,...);
#endif
2.创建sockutil.c文件,编写通用过程connectSock及passiveSock代码
#include "sockutil.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <errno.h>
int connectSock(char* host,char* service,char* protocol)
{
struct hostent* pHost;
struct servent* pServ;
struct protoent* pProto;
struct sockaddr_in addr;
int s,type;
memset(&addr,0,sizeof(addr));
addr.sin_family=AF_INET;
if((pHost=gethostbyname(host))!=NULL)
memcpy(&addr.sin_addr,pHost->h_addr,pHost->h_length);
else if((addr.sin_addr.s_addr=inet_addr(host))==INADDR_NONE)
errexit("can't get \"%s\" host entry",host);
if((pServ=getservbyname(service,protocol))!=NULL)
addr.sin_port=pServ->s_port;
else if((addr.sin_port=htons((unsigned short)atoi(service)))==0)
errexit("can't get \"%s\" service entry",service);
if((pProto=getprotobyname(protocol))==0)
errexit("can't get \"%s\" protocol entry",protocol);
if(strcmp(protocol,"tcp")==0)
type=SOCK_STREAM;
else
type=SOCK_DGRAM;
s=socket(PF_INET,type,pProto->p_proto);
if(s<0)
errexit("can't create socket");
if(connect(s,(struct sockaddr*)&addr,sizeof(addr))<0)
errexit("can't connect to %s:%s",host,service);
return s;
}
int passiveSock(char* service,char* protocol,int qlen)
{
struct servent* pServ;
struct protoent* pProto;
struct sockaddr_in addr;
int s,type,reuse=1;
memset(&addr,0,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=INADDR_ANY;
if((pServ=getservbyname(service,protocol))!=NULL)
addr.sin_port=pServ->s_port;
else if((addr.sin_port=htons((unsigned short)atoi(service)))==0)
errexit("can't get \"%s\" service entry",service);
if((pProto=getprotobyname(protocol))==0)
errexit("can't get \"%s\" protocol entry",protocol);
if(strcmp(protocol,"tcp")==0)
type=SOCK_STREAM;
else
type=SOCK_DGRAM;
s=socket(PF_INET,type,pProto->p_proto);
if(s<0)
errexit("can't create socket");
if(setsockopt(s,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(int))<0)
errexit("setsockopt err");
if(bind(s,(struct sockaddr*)&addr,sizeof(addr))<0)
errexit("can't bind to %s port",service);
if(type==SOCK_STREAM&&listen(s,qlen)<0)
errexit("can't to listen on %s port",service);
return s;
}
void errexit(char* fmt,...)
{
va_list arg_ptr;
va_start(arg_ptr,fmt);
vfprintf(stderr,fmt,arg_ptr);
fprintf(stderr,":%s.\n",strerror(errno));
va_end(arg_ptr);
exit(errno);
}
3.编写头文件server.h
#ifndef SERVER_H
#define SERVER_H
//SERVER_CONFIG
#define SERV_PORT "8888"
//地址链表项目结构体
struct addrItem{
struct sockaddr_in addr;
struct addrItem* next;
};
#endif
4.编写server.c文件,实现UDP单线程循环式聊天服务器
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/select.h>
#include"sockutil.h"
#include"server.h"
#include"msg.h"
//UDP服务器端套接字##
int sockFd;
//地址链表
struct addrItem* addrHead;
struct addrItem* addrTail;
//函数声明
//声明函数1,添加需要广播的地址
struct addrItem* createNewAddrItem(struct sockaddr_in* addr);
//声明函数2,广播消息的函数
int broadcastMsg(struct msg* msg);
//主线程
int main(int argc,char* argv[])
{
//计数
int n;
//IP字符串转换缓冲区
char addrBuf[16];
//套接字用
socklen_t slen;
//消息及账户格式
struct msg* pMsg;
if(argc!=2)
errexit("USAGE:%s port.\n",argv[0]);
sockFd=passiveSock(argv[1],"udp",0);
slen=sizeof(struct sockaddr_in);
while(1)
{
pMsg=buildMsg("");
if((n=recvfrom(sockFd,pMsg->content,MSG_LEN,0,
(struct sockaddr*)&pMsg->addr,&slen))<0)
errexit("recvfrom error");
inet_ntop(AF_INET, (void *)&pMsg->addr.sin_addr, addrBuf, 16);
printf("来自%s(%d)的消息:%s",
addrBuf,ntohs(pMsg->addr.sin_port),pMsg->content);
createNewAddrItem(&pMsg->addr);
broadcastMsg(pMsg);
memset(&pMsg,0,sizeof(pMsg));
}
}
//保存地址
struct addrItem*
createNewAddrItem(struct sockaddr_in* addr)
{
//为地址链表分配内存##
struct addrItem* ai=(struct addrItem*)malloc(sizeof(struct addrItem));
struct addrItem* p;
//if判断,如果传入的地址已经存在于地址链表中,直接返回地址
//否则需要将地址添加到地址链表中##
if(addr==NULL)
return NULL;
for(p=addrHead;p!=NULL;p=p->next){
if(p->addr.sin_addr.s_addr==addr->sin_addr.s_addr &&
p->addr.sin_port==addr->sin_port) {
//已保存该地址,直接返回
return p;
}
}
//未保存该地址,加入地址列表中
ai=(struct addrItem*)malloc(sizeof(struct addrItem));
ai->addr=*addr;
ai->next=NULL;
if(addrHead==NULL)
addrHead=addrTail=ai;
else {
addrTail->next=ai;
addrTail=ai;
}
return ai;
}
//群发消息
int broadcastMsg(struct msg* msg)
{
//循环地址链表,执行sendto向相应地址发送消息##
struct addrItem* p,*ptmp;
if(msg==NULL)
return -1;
for(p=addrHead;p!=NULL;p=p->next){
sendto(sockFd,msg->content,sizeof(struct msg),0,(struct sockaddr*)&p->addr,sizeof(struct sockaddr));
}
return 0;
}
5.编写头文件client.h
#ifndef CLIENT_H
#define CLIENT_H
#define BUFSIZE 100
#endif
6.编写client.c文件,设计select单线程客户端
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/select.h>
#include"client.h"
#include"sockutil.h"
int main(int argc,char* argv[])
{
int ret,n;
int sockFd;
struct sockaddr_in servAddr;
fd_set oset,nset;
char strBuf[BUFSIZE];
//UDP地址绑定到服务器
if(argc!=3)
errexit("USAGE:%s hostname port",argv[0]);
sockFd=connectSock(argv[1],argv[2],"udp");
//初始化fd_set对象
FD_ZERO(&oset);
FD_SET(STDIN_FILENO,&oset);
FD_SET(sockFd,&oset);
nset=oset;
while(1)
{
//根据select的返回,执行相关的消息发送和接收函数
ret=select(4,&nset,NULL,NULL,0);
if(FD_ISSET(sockFd,&nset))
{
memset(strBuf,0,BUFSIZE);
recv(sockFd,strBuf,BUFSIZE,0);
printf("%s",strBuf);
}
if(FD_ISSET(STDIN_FILENO,&nset))
{
fgets(strBuf,BUFSIZE,stdin);
send(sockFd,strBuf,sizeof(strBuf),0);
memset(strBuf,0,BUFSIZE);
}
nset=oset;
}
}
7.编写头文件msg.h
#ifndef MSG_H
#define MSG_H
#include"sockutil.h"
#define MSG_LEN 100//最大消息长度,包含'\0'
//struct msg and configurations
struct msg{
char content[MSG_LEN];//Msg Content
struct sockaddr_in addr;
struct msg* next;//next one pointer
};
//struct msgQue
struct msgQ{
struct msg* head;
struct msg* tail;
};
//control function
//函数详细说明见msg.c
struct msg* buildMsg(char* cont);
int freeMsg(struct msg* pMsg);
struct msgQ* buildMsgQ(void);
int freeMsgQ(struct msgQ* pMsgQ);
struct msg* getFromMsgQ(struct msgQ* pMsgQ);
int appToMsgQ(struct msgQ* pMsgQ,struct msg* pMsg);
#endif//MSG_H
8.编写msg.c文件,用来存储发送的消息内容
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include"msg.h"
//构建消息
//输入:消息内容指针
//输出:指向struct msg类型对象的指针,失败时返回NULL
struct msg* buildMsg(char* cont) {
struct msg* pMsg;
pMsg=(struct msg*)malloc(sizeof(struct msg));
if(pMsg==NULL)
return NULL;
strcpy(pMsg->content,cont);
memset(&pMsg->addr,0,sizeof(struct sockaddr_in));
pMsg->next=NULL; return pMsg;
}
//卸载消息
//输入:指向struct msg类型对象的指针
//输出:正确卸载,返回0;否则,返回-1
int freeMsg(struct msg* pMsg) {
if(pMsg!=NULL){
free(pMsg);
pMsg=NULL;
return 0;
}
return -1;
}
//构建消息队列
//输入:无
//输出:消息队列对象指针,失败时返回NULL
struct msgQ* buildMsgQ(void) {
struct msgQ* pMsgQ= (struct msgQ*)malloc(sizeof(struct msgQ));
if(pMsgQ==NULL)
return NULL;
pMsgQ->head=NULL;
pMsgQ->tail=NULL;
return pMsgQ;
}
//卸载消息队列
//输入:指向消息队列对象的指针
//输出:成功返回0,失败返回-1
int freeMsgQ(struct msgQ* pMsgQ) {
struct msg* pMsg,*tmp;
if(pMsgQ!=NULL){
pMsg=pMsgQ->head;
while(pMsg!=NULL){
tmp=pMsg->next;
free(pMsgQ);
pMsg=tmp;
}
free(pMsgQ);
pMsgQ=NULL;
return 0;
}
return -1;
}
//获取消息队列头部消息
//输入:消息队列指针
//输出:头部消息的指针,如果队列为空或发生错误,则返回NULL
struct msg* getFromMsgQ(struct msgQ* pMsgQ) {
struct msg* pHead;
if(pMsgQ==NULL)
return NULL;
if(pMsgQ->head==NULL)
return NULL;
pHead=pMsgQ->head;
if(pMsgQ->head==pMsgQ->tail)
pMsgQ->head=pMsgQ->tail=NULL;
else
pMsgQ->head=pMsgQ->head->next;
return pHead;
}
//在消息队列尾部加入消息队列
//输入:消息队列指针,消息对象指针
//输出:操作成功返回0,操作失败返回-1
int appToMsgQ(struct msgQ* pMsgQ,struct msg* pMsg) {
if(pMsgQ==NULL || pMsg==NULL)
return -1;
if(pMsgQ->head==NULL)
pMsgQ->head=pMsgQ->tail=pMsg;
pMsgQ->tail->next=pMsg;
pMsgQ->tail=pMsg;
}
9.编写makefile文件,对已编写完成的代码进行编译运行
SERVEROBJ=msg.o server.o sockutil.o
CLIENTOBJ=client.o sockutil.o
server:${SERVEROBJ}
gcc ${SERVEROBJ} -o server
client:${CLIENTOBJ}
gcc ${CLIENTOBJ} -o client
clean:
rm -rf ${SERVEROBJ} server client
简要说明:Makefile 文件描述了整个工程的编译、连接等规则。为工程编写Makefile 的好处是能够使用一行命令来完成“自动化编译”。SERVEROBJ、CLIENTOBJ为定义的一个变量,gcc命令为编译变量中的文件,-o用来设置编译后的输出文件名称。
rm命令用来删除指定的文件或目录 -r表明同时删除目录下的所有子目录,-f表明强行删除文件或目录,不提示任何信息。
10.显示实验结果
执行命令:make clean server client 对文件进行编译。
执行命令:./server 7777运行服务器端。
再开启一个新窗口,执行命令:./client localhost 7777运行客户端。
在客户端输入任意数据,服务器端能显示接收到的数据则试验成功。