过早的优化是万恶之源
服务器程序(select模型):
这是一个TCP回射服务器程序,绑定套接字,使用select管理监听套接字和连接套接字。处理
内容是把客户端发过来的数据发回去 具体步骤如下
- 创建监听套接字listenfd------------------listenfd = socket(PF_INET, SOCK_STREAM, 0)
- 创建并初始化服务器IPv4结构体-------------struct sockaddr_in servaddr
- 设置REUSEADDR选项以避免TIMEWAIT状态------setsockopt()
- 绑定监听套接字和服务器地址结构(前两者)--bind (listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)
- 开始监听--------------------------------listen (listenfd, SOMAXCONN)
- select在一个while循环中检测事件,无事件到来就一直循环
- select检测到事件,个数为nready:是listenfd吗?--转8 是conn吗?--转9
- accept函数建立连接,把得到的conn放到客户端列表中,nready减一 。nready不为空说明有conn,转9,否则没有别的消息到来,回到第6步
- readn函数在一个循环中检测哪个conn有事件,找到了就读取conn中的消息,回射回去,nready-- ,nready为空说明消息都处理完,回到第6步,否则在本循环中继续。
- 回到第6步
要点
- 每个函数都有错误检测和处理
- 发的时候换成网络字节序htonl,收过来换成主机字节序ntohl
- 使用readn writen封装读写函数
- 发时直接发4+n;收时先收4字节包头,由包头得知包体长为n,再收n字节包体
*TCP是一种流协议,像流水一样,没有边界。收方不知道一次该读取多少,再加上TCP的复杂机制就会产生粘包问题。解决方案:
*1.定长包:双方协定好每次收发多少字节
*2.包尾加上\r\n,如ftp http协议:确定消息边界
*3.包头加上包体长度:先接收4字节包头,再接收x字节包体,如下readn函数
*4.更复杂的应用层协议
*关键问题在于read/write函数,每次读取的字节数不一定是指定字节数
*粘包问题只存在广域网中??
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
struct packet{//定义数据包格式
int len;
char buf[1024];
};
ssize_t readn(int fd,void *buf,size_t count){//每次读取n字节的函数
size_t nleft=count;//剩余未被读取的字节数
size_t nread;//已接收的字节数
char *bufp=(char *)buf;//记录buf
while(nleft>0){//只要没读完要求的字节数
if((nread=read(fd,bufp,nleft))<0){
//nread记录读取的字节数,不一定=nleft
if(errno=EINTR) continue;//被信号中断
return -1;//其他情况的读取失败,退出
}
else if(nread==0) return count-nleft;//对等方关闭 返回
bufp+=nread;//指针偏移,指向未被读取的字节
nleft-=nread;//剩余nleft未读,回到while循环开始
}
return count;//nleft为0 都读完了 返回
}
ssize_t writen(int fd,void *buf,size_t count){
size_t nleft=count;//剩余未被写入的字节数
size_t nwrite;//已写入的字节数
char *bufp=(char *)buf;//记录buf
while(nleft>0){
if((nwrite=write(fd,bufp,nleft))<0){
//nwrite记录写的字节数,不一定=nleft
if(errno=EINTR) continue;//被信号中断
return -1;//其他情况的读取失败,退出
}
else if(nwrite==0) continue;//什么都没写
bufp+=nwrite;//指针偏移,指向未被读取的字节
nleft-=nwrite;//剩余nleft未读,回到while循环开始
}
return count;//nleft为0 都写完了 返回
}
void handle_sigchld(int sig){
while(waitpid(-1,NULL,WNOHANG)>0);//wait all child process
}
int main(){
signal(SIGCHLD,handle_sigchld);
printf("服务器主进程:%d\n",getpid());
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket");
printf("socket()-新建套接字成功\n");
struct sockaddr_in servaddr;//IPv4地址结构
memset(&servaddr,0,sizeof(servaddr));//用0填充
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(7777);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//任意本机地址,方法1
//inet_aton("127.0.0.1",&servaddr.sin_addr);//方法2
//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//方法3
int on = 1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
ERR_EXIT("setsocketopt");//设置REUSEADDR选项 避免 TIMEWAIT状态
if (bind (listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
ERR_EXIT("bind");//把IP地址结构强制转化为socket结构
printf("bind()-监听套接字和服务器地址绑定成功\n");
if (listen (listenfd, SOMAXCONN) < 0)//队列最大值 并发连接数
ERR_EXIT("listen");
printf("listen()-监听套接字开始监听\n");
struct sockaddr_in peeraddr;//对方地址
socklen_t peerlen = sizeof(peeraddr);//peerlen 要有初始值
int conn;//conn记录新返回的套接字 -已连接套接字-从已连接队列移除
int maxfd=listenfd;//最大套接字描述符
fd_set rset,allset;
FD_ZERO(&rset);
FD_ZERO(&allset);//要管理的所有套接字,包括listenfd 每个客户端的conn
FD_SET(listenfd,&allset);
int client[FD_SETSIZE];//client数组表示目前连接上的客户端,-1表示空,其他数字代表客户端编号(套接字)
for(int i=0;i<FD_SETSIZE;i++){
client[i]=-1;//初始化为-1
}
while(1){//主处理程序
rset=allset;//allset每次添加了conn 赋值给rset来监听 不是很懂
int nready=select(maxfd+1,&rset,NULL,NULL,NULL);//开始探路
if(nready == -1) {
if(errno==EINTR) continue;//EINTR信号可以忽略
ERR_EXIT("select");//其他问题导致select出错
}
if(nready==0) continue;//未检测到任何待处理套接字,就回到开始
if(FD_ISSET(listenfd,&rset)){//检测到了,是不是listenfd,有连接过来了?
printf("select 检测到listenfd不为空\n");
conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);
//这个时候调用accept就不可能阻塞了,然后从ESTABLISHED队列里返回第一个连接
if(conn==-1) ERR_EXIT("accept");
int i;
for(i=0;i<FD_SETSIZE;i++){
if(client[i]<0){//如果client数组里住了一个客户端,就不等于-1,找下一个为-1的元素
client[i]=conn;//存放一个连接到客户端列表数组中
break;//跳出for循环
}
}
if(i==FD_SETSIZE){//一直往后找,找到最大值了(这个最大值怎么看?)
printf("too many conn\n");//说明连接已满
}
printf("和客户端%d,IP:%s::%d已进行三次握手\n",conn,inet_ntoa(peeraddr.sin_addr),htons(peeraddr.sin_port));
FD_SET(conn,&allset);//放到allset里同时更新最大fd
if(conn>maxfd) maxfd=conn;
if(--nready<=0) continue;/*已经处理了一个套接字,nready--,如果小于0 说明没有需要处理的,回到开头;如果
大于0 说明还有别的conn */
}
for(int i=0;i<FD_SETSIZE;i++){//现在确定conn有响应了(消息到来),但是这么多conn,我不知道是哪个啊,所以遍历
conn=client[i];
if(conn==-1) continue;//这个是空的 就再往后找
if(FD_ISSET(conn,&rset)){//找到一个不为空,而且这个conn确实有消息到来了
printf("select 检测到%d号已连接套接字有消息到来\n",conn);
struct packet recvbuf;//接收消息队列
int ret = readn(conn,&recvbuf.len,4);//先接收4字节
if (ret == -1) ERR_EXIT("read");
else if(ret<4){
printf("客户端%d关闭\n",conn);
FD_CLR(conn,&allset);//移除一个conn
break;
}
int n=ntohl(recvbuf.len);//len是网络字节序,转换成主机字节序
ret=readn(conn,recvbuf.buf,n);//再接收n字节
if (ret == -1) ERR_EXIT("read");
else if(ret<4){
printf("客户端%d关闭\n",conn);
FD_CLR(conn,&allset);
break;
}
printf("%d号套接字说:",conn);
fputs(recvbuf.buf,stdout);//输出到屏幕
strcat(recvbuf.buf, " received");
writen(conn,&recvbuf,4+n);//回射回去
memset(&recvbuf,0,sizeof(recvbuf));
if(--nready<=0) continue;//处理完一个conn了 nready减一,如果小于0都处理完了,返回select,
//否则说明还有conn没处理 返回for循环
}
}
}
return 0;
}
运行截图(ubantu ) 左上角为服务器,其他三个为客户端
服务器程序(多进程模型)
这是一个TCP回射服务器程序,绑定套接字,每当有客户端过来就新开进程处理。处理
内容是把客户端发过来的数据发回去 具体步骤如下
1创建监听套接字listenfd------------------listenfd = socket(PF_INET, SOCK_STREAM, 0)
2创建并初始化服务器IPv4结构体-------------struct sockaddr_in servaddr
3设置REUSEADDR选项以避免TIMEWAIT状态------setsockopt()
4绑定监听套接字和服务器地址结构(前两者)--bind (listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)
5开始监听--------------------------------listen (listenfd, SOMAXCONN)
6创建对方IPv4结构体-----------------------struct sockaddr_in peeraddr
7接受连接并返回一个通信套接字--------------conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)
8三次握手完成,新建子进程来处理------------doit(conn) close(listenfd)
doit()-----------readn(conn,&recvbuf.len,4) n=ntohl(recvbuf.len);
readn(conn,recvbuf.buf,n); writen(conn,&recvbuf,4+n);
9如果对方关闭,read返回0,退出子进程 。否则在while循环中处理
10主进程关闭conn套接字 回到第7步循环
11退出主进程
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
/*
*TCP是一种流协议,像流水一样,没有边界。收方不知道一次该读取多少,再加上TCP的复杂机制就会产生粘包问题。解决方案:
*1.定长包:双方协定好每次收发多少字节
*2.包尾加上\r\n,如ftp http协议:确定消息边界
*3.包头加上包体长度:先接收4字节包头,再接收x字节包体,如下readn函数
*4.更复杂的应用层协议
*关键问题在于read/write函数,每次读取的字节数不一定是指定字节数
*粘包问题只存在广域网中??
*/
struct packet{//定义数据包格式
int len;
char buf[1024];
};
ssize_t readn(int fd,void *buf,size_t count){//每次读取n字节的函数
size_t nleft=count;//剩余未被读取的字节数
size_t nread;//已接收的字节数
char *bufp=(char *)buf;//记录buf
while(nleft>0){//只要没读完要求的字节数
if((nread=read(fd,bufp,nleft))<0){
//nread记录读取的字节数,不一定=nleft
if(errno=EINTR) continue;//被信号中断
return -1;//其他情况的读取失败,退出
}
else if(nread==0) return count-nleft;//对等方关闭 返回
bufp+=nread;//指针偏移,指向未被读取的字节
nleft-=nread;//剩余nleft未读,回到while循环开始
}
return count;//nleft为0 都读完了 返回
}
ssize_t writen(int fd,void *buf,size_t count){
size_t nleft=count;//剩余未被写入的字节数
size_t nwrite;//已写入的字节数
char *bufp=(char *)buf;//记录buf
while(nleft>0){
if((nwrite=write(fd,bufp,nleft))<0){
//nwrite记录写的字节数,不一定=nleft
if(errno=EINTR) continue;//被信号中断
return -1;//其他情况的读取失败,退出
}
else if(nwrite==0) continue;//什么都没写
bufp+=nwrite;//指针偏移,指向未被读取的字节
nleft-=nwrite;//剩余nleft未读,回到while循环开始
}
return count;//nleft为0 都写完了 返回
}
void echo_server(int conn){//开始接客
struct packet recvbuf;//接收消息队列
while(1){
memset(&recvbuf,0,sizeof(recvbuf));//初始化数组
int ret = readn(conn,&recvbuf.len,4);//先接收4字节
if (ret == -1) ERR_EXIT("read");
else if(ret<4){
printf("客户端%d关闭\n",conn);
break;
}
int n=ntohl(recvbuf.len);//len是网络字节序,转换成主机字节序
ret=readn(conn,recvbuf.buf,n);
if (ret == -1) ERR_EXIT("read");
else if(ret<4){
printf("客户端%d关闭\n",conn);
break;
}
printf("收:");
fputs(recvbuf.buf,stdout);//输出到屏幕
writen(conn,&recvbuf,4+n);//回射回去
}
}
void handle_sigchld(int sig){
while(waitpid(-1,NULL,WNOHANG)>0);//wait all child process
}
int main(){
signal(SIGCHLD,handle_sigchld);
printf("服务器主进程:%d\n",getpid());
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket");
printf("socket()-新建套接字成功\n");
struct sockaddr_in servaddr;//IPv4地址结构
memset(&servaddr,0,sizeof(servaddr));//用0填充
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(7777);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//任意本机地址
//inet_aton("127.0.0.1",&servaddr.sin_addr);//方法2
//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//方法3
int on = 1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
ERR_EXIT("setsocketopt");//设置REUSEADDR选项 避免 TIMEWAIT状态
if (bind (listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
ERR_EXIT("bind");//把IP地址结构强制转化为socket结构
printf("bind()-监听套接字和服务器地址绑定成功\n");
if (listen (listenfd, SOMAXCONN) < 0)//队列最大值 并发连接数
ERR_EXIT("listen");
printf("listen()-监听套接字开始监听\n");
struct sockaddr_in peeraddr;//对方地址
socklen_t peerlen = sizeof(peeraddr);//peerlen 要有初始值
int conn;//conn记录新返回的套接字 -已连接套接字-从已连接队列移除
pid_t pid;//新建进程
while (1){//开始操作
if ((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)
ERR_EXIT("accept");//从ESTABLISHED队列里返回第一个连接 空则阻塞
printf("和客户端%d,IP:%s::%d已进行三次握手\n",conn,inet_ntoa(peeraddr.sin_addr),htons(peeraddr.sin_port));
pid=fork();
if(pid == -1){
ERR_EXIT("fork");//新建子进程失败 退出主进程
}
if(pid == 0){
printf("新建子进程:%d\n",getpid());
close(listenfd);
echo_server(conn);
exit(EXIT_SUCCESS);//一旦客户端实例关闭了 这个处理的进程就没有存在的价值 要退出 否则子进程也会去accept
}else{
close(conn);
}
}
return 0;
}
客户端程序
/*这是一个TCP回射客户端程序,绑定套接字,去连接服务器,收发消息
1创建主动套接字sock-----------------------sock = socket(PF_INET, SOCK_STREAM, 0))
2创建并初始化服务器IPv4结构体-------------struct sockaddr_in servaddr
3用自己的套接字连接服务器-----------------connect(sock,(struct sockaddr *)&servaddr,sizeof(servaddr))
4三次握手完成,进入while循环{read (sock) write(sock)},循环条件是能从stdin读取字符(不会自动停止)
5退出进程
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
/*perror()-print a system error message
exit()-cause normal process termination*/
struct packet {
int len;//包头 存放包体的实际长度
char buf[1024];//包体 缓冲区
};
ssize_t readn(int fd,void *buf,size_t count) {
size_t nleft=count;//剩余未被读取的字节数
size_t nread;//已接收的字节数
char *bufp=(char *)buf;//记录buf
while(nleft>0) //只要没读完要求的字节数
{
if((nread=read(fd,bufp,nleft))<0)
{
//nread记录读取的字节数,不一定=nleft
if(errno=EINTR) continue;//被信号中断
return -1;//其他情况的读取失败,退出
}
else if(nread==0) return count-nleft;//对等方关闭 返回
bufp+=nread;//指针偏移,指向未被读取的字节
nleft-=nread;//剩余nleft未读,回到while循环开始
}
return count;//nleft为0 都读完了 返回
}
ssize_t writen(int fd,void *buf,size_t count){
size_t nleft=count;//剩余未被写入的字节数
size_t nwrite;//已写入的字节数
char *bufp=(char *)buf;//记录buf
while(nleft>0)
{
if((nwrite=write(fd,bufp,nleft))<0)
{
//nwrite记录写的字节数,不一定=nleft
if(errno=EINTR) continue;//被信号中断
return -1;//其他情况的读取失败,退出
}
else if(nwrite==0) continue;//什么都没写
bufp+=nwrite;//指针偏移,指向未被读取的字节
nleft-=nwrite;//剩余nleft未读,回到while循环开始
}
return count;//nleft为0 都写完了 返回
}
void echo_client(int sock){
struct packet sendbuf;//发送的数据包
memset(&sendbuf,0,sizeof(sendbuf));
struct packet recvbuf;//接收的数据包
fd_set ret;
FD_ZERO(&ret);
int fd_stdin=fileno(stdin);
int maxfd=fd_stdin>sock?fd_stdin:sock;
int nready;//ready things number
while(1){
FD_SET(fd_stdin,&ret);//
FD_SET(sock,&ret);
nready=select(maxfd+1,&ret,NULL,NULL,NULL);
if(nready==-1) ERR_EXIT("select");
if(nready==0) continue;
if(FD_ISSET(sock,&ret)){//检测到服务器的消息
printf("select() 检测到服务器的可读事件\n");
int ret = readn(sock,&recvbuf.len,4);//先接收4字节
if (ret == -1) ERR_EXIT("read");
else if(ret<4){
printf("客户端%d关闭\n",sock);
break;
}
printf("服务器说:");
int n=ntohl(recvbuf.len);//len是网络字节序,转换成主机字节序
ret=readn(sock,recvbuf.buf,n);
if (ret == -1) ERR_EXIT("read");
else if(ret<4)
{
printf("server%d关闭\n",sock);
break;
}
fputs(recvbuf.buf,stdout);//输出到屏幕
memset(&recvbuf,0,sizeof(recvbuf));
}
if(FD_ISSET(fd_stdin,&ret)){//检测到屏幕有输入
printf("select() 检测到屏幕有输入\n");
if(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin)==NULL) break;
int n=strlen(sendbuf.buf);
printf("发生长度为%d 发送内容为%s\n",n,sendbuf.buf);
sendbuf.len=htonl(n);//换成网络字节序
writen (sock,&sendbuf,4+n);//senbbuf写入套接字 包头四字节
memset(&sendbuf,0,sizeof(sendbuf));
}
}
close(sock);
}
int main()
{
printf("note:at least input 3 byte chars\n");
printf("本客户端进程:%d\n",getpid());
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket");
printf("socket()-新建套接字成功\n");
struct sockaddr_in servaddr;//IPv4地址结构
memset(&servaddr,0,sizeof(servaddr));//用0填充
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(7777);
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//任意本机地址
//inet_aton("127.0.0.1",&servaddr.sin_addr);//方法2
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//方法3
if((connect(sock,(struct sockaddr *)&servaddr,sizeof(servaddr))) < 0)
ERR_EXIT("connect");
printf("和服务器%s:%d已进行三次握手\n",inet_ntoa(servaddr.sin_addr),htons(servaddr.sin_port));
echo_client(sock);
close(sock);
printf("关闭套接字\n");
return 0;
}