【Linux网络编程】TCP回射服务器\客户端

过早的优化是万恶之源

ã(åå)é温socket tcpç¼ç¨ã

服务器程序(select模型):

这是一个TCP回射服务器程序,绑定套接字,使用select管理监听套接字和连接套接字。处理
    内容是把客户端发过来的数据发回去 具体步骤如下

  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. select在一个while循环中检测事件,无事件到来就一直循环
  7. select检测到事件,个数为nready:是listenfd吗?--转8 是conn吗?--转9
  8. accept函数建立连接,把得到的conn放到客户端列表中,nready减一 。nready不为空说明有conn,转9,否则没有别的消息到来,回到第6步
  9. readn函数在一个循环中检测哪个conn有事件,找到了就读取conn中的消息,回射回去,nready-- ,nready为空说明消息都处理完,回到第6步,否则在本循环中继续。
  10. 回到第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;
}


发布了24 篇原创文章 · 获赞 12 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Protocols7/article/details/85712758