unix 网络通讯模型_unix网络编程模型 - CSDN
精华内容
参与话题
  • Linux 网络 I/O 模型简介(图文)

    万次阅读 2017-06-01 11:45:24
    1、介绍 Linux 的内核将所有外部设备都看做一个文件来操作(一切皆文件),对一个文件的读写操作会调用内核... 根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型。 1.1、阻塞I/O模型 最常用的I/O模型,默认

    转载请注明出处:http://blog.csdn.net/anxpp/article/details/51503329,谢谢!

    1、介绍

        Linux 的内核将所有外部设备都看做一个文件来操作(一切皆文件),对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有响应的描述符,称为socket fd(socket文件描述符),描述符就是一个数字,指向内核中的一个结构体(文件路径,数据区等一些属性)。

        根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型。

        1.1、阻塞I/O模型

        最常用的I/O模型,默认情况下,所有文件操作都是阻塞的。

        比如I/O模型下的套接字接口:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直等待。

        进程在调用recvfrom开始到它返回的整段时间内都是被阻塞的,所以叫阻塞I/O模型。

        图示:

        01

        1.2、非阻塞I/O模型

        recvfrom从应用层到内核的时候,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。

        图示:

        02

        1.3、I/O复用模型

        Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样,select/poll可以帮我们侦测多个fd是否处于就绪状态。

        select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。

        Linux还提供一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback。

        图示:

        03

        1.4、信号驱动I/O模型

        首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,非阻塞)。当数据准备就绪时,就为改进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理树立。

        图示:

        04

        1.5、异步I/O

        告知内核启动某个操作,并让内核在整个操作完成后(包括数据的复制)通知进程。

        信号驱动I/O模型通知的是何时可以开始一个I/O操作,异步I/O模型有内核通知I/O操作何时已经完成。

        图示:

        05

    2、I/O多路复用技术

        I/O编程中,需要处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。

        正如前面的简介,I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。

        与传统的多线程模型相比,I/O多路复用的最大优势就是系统开销小,系统不需要创建新的额外线程,也不需要维护这些线程的运行,降低了系统的维护工作量,节省了系统资源。

        主要的应用场景:

    •     服务器需要同时处理多个处于监听状态或多个连接状态的套接字。
    •     服务器需要同时处理多种网络协议的套接字。

        支持I/O多路复用的系统调用主要有select、pselect、poll、epoll。

        而当前推荐使用的是epoll,优势如下:

    •     支持一个进程打开的socket fd不受限制。
    •     I/O效率不会随着fd数目的增加而线性下将。
    •     使用mmap加速内核与用户空间的消息传递。
    •     epoll拥有更加简单的API。

    3、Java中的网络IO编程

        如果只是做Java开发,以上内容只需了解即可,不必深究(随便说说而已)。

        已专门出了文章介绍:Java 网络IO编程总结(BIO、NIO、AIO均含完整实例代码)

    展开全文
  • 概述Linux下进程通讯方式有很多,比较典型的有套接字,平时比较常用的套接字是基于TCP/IP协议的,适用于两台不同主机上两个进程间通信, 通信之前需要指定IP地址. 但是如果同一台主机上两个进程间通信用套接字,还需要...

    概述

    Linux下进程通讯方式有很多,比较典型的有套接字,平时比较常用的套接字是基于TCP/IP协议的,适用于两台不同主机上两个进程间通信, 通信之前需要指定IP地址. 但是如果同一台主机上两个进程间通信用套接字,还需要指定ip地址,有点过于繁琐. 这个时候就需要用到UNIX Domain Socket, 简称UDS,
    UDS的优势:

    • UDS传输不需要经过网络协议栈,不需要打包拆包等操作,只是数据的拷贝过程
    • UDS分为SOCK_STREAM(流套接字)和SOCK_DGRAM(数据包套接字),由于是在本机通过内核通信,不会丢包也不会出现发送包的次序和接收包的次序不一致的问题

    流程介绍

    如果熟悉Socket的话,UDS也是同样的方式, 区别如下:

    • UDS不需要IP和Port, 而是通过一个文件名来表示
    • domain 为 AF_UNIX
    • UDS中使用sockaddr_un表示
    struct sockaddr_un {
        sa_family_t sun_family; /* AF_UNIX */
        char sun_path[UNIX_PATH_MAX];   /* pathname */
    };

    服务端: socket -> bind -> listen -> accet -> recv/send -> close
    客户端: socket -> connect -> recv/send -> close

    函数介绍

    • 开始创建socket
     int socket(int domain, int type, int protocol)
     domain(域) : AF_UNIX 
     type : SOCK_STREAM/ SOCK_DGRAM : 
     protocol : 0

    SOCK_STREAM(流) : 提供有序,可靠的双向连接字节流。 可以支持带外数据传输机制,
    无论多大的数据都不会截断
    SOCK_DGRAM(数据报):支持数据报(固定最大长度的无连接,不可靠的消息),数据报超过最大长度,会被截断.

    • 获取到socket文件描述符之后,还要将其绑定一个文件上
     int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    sockfd : 传入sock的文件描述符
    addr : 用sockaddr_un表示
    addrlen : 结构体长度
    struct sockaddr_un {
        sa_family_t sun_family; /* AF_UNIX */
        char sun_path[UNIX_PATH_MAX];   /* pathname */
    };
    • 监听客户端的连接
    int listen(int sockfd, int backlog);
    sockfd : 文件描述符
    backlog : 连接队列的长度
    • 接受客户端的连接
    int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
    UDS不存在客户端地址的问题,因此这里的addr和addrlen参数可以设置为NULL

    Demo程序

    • uds-server.c
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<sys/stat.h>
    #include<sys/socket.h>
    #include<sys/types.h>
    #include<sys/un.h>
    #include<errno.h>
    #include<stddef.h>
    #include<unistd.h>
    
    #define MAX_CONNECT_NUM 2
    #define BUFFER_SIZE 1024
    const char *filename="uds-tmp";
    
    int main()
    {
        int fd,new_fd,len,i;
        struct sockaddr_un un;
        fd = socket(AF_UNIX,SOCK_STREAM,0);
        if(fd < 0){
            printf("Request socket failed!\n");
            return -1;
        }
        un.sun_family = AF_UNIX;
        unlink(filename);
        strcpy(un.sun_path,filename);
        if(bind(fd,(struct sockaddr *)&un,sizeof(un)) <0 ){
            printf("bind failed!\n");
            return -1;
        }
        if(listen(fd,MAX_CONNECT_NUM) < 0){
            printf("listen failed!\n");
            return -1;
        }
        while(1){
            struct sockaddr_un client_addr;
            char buffer[BUFFER_SIZE];
            bzero(buffer,BUFFER_SIZE);
            len = sizeof(client_addr);
            //new_fd = accept(fd,(struct sockaddr *)&client_addr,&len);
            new_fd = accept(fd,NULL,NULL);
            if(new_fd < 0){
                printf("accept failed\n");
                return -1;
            }
            int ret = recv(new_fd,buffer,BUFFER_SIZE,0);
            if(ret < 0){
                printf("recv failed\n");
            }
            for(i=0; i<10; i++){
                printf(" %d",buffer[i]);
            }
            close(new_fd);
            break;
        }
        close(fd);
    }
    
    • uds-client.c
    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<sys/stat.h>
    #include<sys/socket.h>
    #include<sys/types.h>
    #include<sys/un.h>
    #include<errno.h>
    #include<stddef.h>
    #include<unistd.h>
    #define BUFFER_SIZE 1024
    const char *filename="uds-tmp";
    
    int main()
    {
        struct sockaddr_un un;
        int sock_fd;
        char buffer[BUFFER_SIZE] = {1,2,3};
        un.sun_family = AF_UNIX;
        strcpy(un.sun_path,filename);
        sock_fd = socket(AF_UNIX,SOCK_STREAM,0);
        if(sock_fd < 0){
            printf("Request socket failed\n");
            return -1;
        }
        if(connect(sock_fd,(struct sockaddr *)&un,sizeof(un)) < 0){
            printf("connect socket failed\n");
            return -1;
        }
        send(sock_fd,buffer,BUFFER_SIZE,0);
    
        close(sock_fd);
        return 0;
    }
    

    参考

    展开全文
  • 简易的TCP C/S模型实现

    以下内容主要参考书籍《Linux C编程一站式学习》、《Unix网络编程》、《Unix高级环境编程》

    首先要明确客户端与服务器要怎么去实现通信
    下图便是一个简易的TCP C/S模型实现
    这里写图片描述

    知道模型之后,接下来只是一些与网络接口相关的API调用。

    预备知识:
    socket是什么?

    1.在TCP 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP地址+端口号”就称为socket。
    2.在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。socket本身有“插座”的意思,因此用来描述网络连接的一对一关系。
    3.TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket接口
    TCP4层模型与socket接口之间关系如下图
    这里写图片描述

    具体相关的函数接口
    1.socket()

    #include <sys/types.h>
    #include <sys/socket.h>
    int socket(int domain,int type,int protocol);
    各参数解释如下
    
    domain:
        AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
        AF_INET6 与上面类似,不过是来用IPv6的地址
        AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
    
    type:
        SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。当将protocol设为0时,则默认使用TCP来进行传输。
        SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,当将protocol设为0时,则默认使用UDP来进行传输。
        SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
        SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
        SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
    
    protocol: 
        0 默认协议
    
    返回值:
        成功返回一个新的文件描述符
        失败返回-1,并设置errno
    

    当进程调用socket函数后,系统会为其分配一个文件描述符,供进程去在网络上进行通信,对于文件描述符,我们就有许多对应的系统调用可以使用了,比如read,write等等。
    但对于服务器来说,需要将本地地址绑定一个固定端口及相关的接口,来让客户端能够方便的访问到服务器,这时就需要一个函数接口bind()。

    2.bind()

    #include <sys/types.h> /* See NOTES */
    #include <sys/socket.h>
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    sockfd:
        socket文件描述符
    addr:
        构造出IP地址加端口号
    addrlen:
        sizeof(addr)长度
    这个函数重点在于struct sockaddr这个结构体,由于历史原因,当前并没有void *这个向上兼容的泛型指针,而所有的这些函数参数都使用struct sockaddr *类型表示,在使用的时候需要我们强转一下。
    struct sockaddr {
    sa_family_t sa_family; /* address family, AF_xxx */
    char sa_data[14]; /* 14 bytes of protocol address */
    };
    
    IPV4地址使用struct sockaddr_in
    struct sockaddr_in {
    __kernel_sa_family_t sin_family; /* Address family */ IPV4此值取值为AF_INET
    __be16 sin_port; /* Port number */
    struct in_addr sin_addr; /* Internet address */
    /* Pad to size of `struct sockaddr'. */
    unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
    sizeof(unsigned short int) - sizeof(struct in_addr)];
    };
    /* Internet address. */
    struct in_addr {
    __be32 s_addr;
    };
    
    IPV6使用struct sockaddr_in6
    struct sockaddr_in6 {
    unsigned short int sin6_family; /* AF_INET6 */  
    __be16 sin6_port; /* Transport layer port # */
    __be32 sin6_flowinfo; /* IPv6 flow information */
    struct in6_addr sin6_addr; /* IPv6 address */
    __u32 sin6_scope_id; /* scope id (new in RFC2553) */
    };
    struct in6_addr {
    union {
    __u8 u6_addr8[16];
    __be16 u6_addr16[8];
    __be32 u6_addr32[4];
    } in6_u;
    #define s6_addr in6_u.u6_addr8
    #define s6_addr16 in6_u.u6_addr16
    #define s6_addr32 in6_u.u6_addr32
    };
    
    本地套接字使用struct sockaddr_un
    #define UNIX_PATH_MAX 108
    struct sockaddr_un {
    __kernel_sa_family_t sun_family; /* AF_UNIX */
    char sun_path[UNIX_PATH_MAX]; /* pathname */
    };
    返回值:
        成功返回0,失败返回-1, 并设置errno

    当服务器将本地地址和一套接口绑定完之后,则需要给服务器设置为监听状态,使其有接受连接请求的能力,此时就需要函数接口listen()。
    3.listen()

    #include <sys/types.h> /* See NOTES */
    #include <sys/socket.h>
    int listen(int sockfd, int backlog);
    listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
    sockfd:
        socket文件描述符
    backlog:
        排队建立3次握手队列和刚刚建立3次握手队列的链接数和
    查看系统默认backlog(128)
    cat /proc/sys/net/ipv4/tcp_max_syn_backlog

    当设置好监听状态后,服务器调用accept()接受连接,如果服务器没有接受到客户端的请求,就会处于阻塞状态,直到有客户端连接上来为止。

    4.accept()

    #include <sys/types.h> /* See NOTES */
    #include <sys/socket.h>
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    sockdf:
        socket文件描述符
    addr:
        传出参数,返回链接客户端地址信息,含IP地址和端口号
    addrlen:
        传入传出参数,传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客
    户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。
    返回值:
        成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,并设置errno

    到此时服务器基本架构就基本完成了,就只要阻塞I等待客户端连接就好了,具体的实现代码如下,为了将模型尽可能简单化,至此,我就没有写太多关于出错的信息的操作,以免淹没于细节中。

    /*sever.c*/
    #include <sys/types.h>                                                                              
    #include <sys/socket.h>                                                                             
    #include <strings.h>                                                                                
    #include <ctype.h>                                                                                  
    #include <stdio.h>                                                                                  
    #include <stdlib.h>                                                                                 
    #include <unistd.h>                                                                                 
    #include <arpa/inet.h>                                                                              
    #define SERV_PORT 8000                                                                              
    void perr_exit(const char *str)                                                                     
    {                                                                                                   
        perror(str);                                                                                    
        exit(1);                                                                                        
    }                                                                                                   
    int main()                                                                                          
    {                                                                                                   
        int sfd = socket(AF_INET,SOCK_STREAM,0),cfd;                                                    
        struct sockaddr_in servaddr;                                                                    
        struct sockaddr_in client_addr;                                                                 
        int i,len;                                                                                      
        socklen_t addr_len;                                                                             
        //init                                                                                          
        bzero(&servaddr,sizeof(struct sockaddr_in));                                                    
        servaddr.sin_family = AF_INET;                                                                  
    
        //htons htonl 都属于网络字节序转换,在代码段之后会进行解释,就先理解为转换为网络中所需要的类型  
        servaddr.sin_port = htons(SERV_PORT);                                                           
        //INADDR_ANY表示任意都可连接(因为客户端不是来自同一个网络地址)                                
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//                                                 
        if(bind(sfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0)                                 
            perr_exit("bind error");                                                                    
    
        //设置可连接数为128                                                                             
        listen(sfd,128);                                                                                
    
        printf("wait for conncet---------\n");                                                          
        addr_len = sizeof(client_addr);                                                                 
        cfd = accept(sfd,(struct sockaddr *)&client_addr,&addr_len);                                    
        if(cfd == -1)                                                                                   
            perr_exit("accept error");                                                                  
        char buf[256];                                                                                  
    
        /*系统还为我们封装了IP地址转换函数                                                              
         * 因为IP地址在网络中为网络字节序二进制值,而我们平常使用的是ASCII字符串,                      
         * 故有这一组函数来进行转换,其实也不难记,可以这样形式的记住,以免用混ip to net,net to ip      
         * #include <arpa/inet.h>                                                                       
         * int inet_pton(int af, const char *src, void *dst);                                           
         * const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);                   
         * */                                                                                           
        printf("client IP :%s %d\n",                                                                    
                inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,buf,sizeof(buf)),                        
              ntohs(client_addr.sin_port));                                                             
        while(1)                                                                                        
        {                                                                                               
            len = read(cfd,buf,sizeof(buf)); //读取客户端的数据                                         
            if(len == -1)                                                                               
                perr_exit("read error");
            /*
              if(len == 0)                                                                                
            {                                                                                           
                printf("the other size closed\n");                                                             
                close(cfd);                                                                             
                close(sfd);                                                                             
                exit(1);                                                                                
            }           
            */                                                                    
            if(write(STDOUT_FILENO,buf,len) < 0) //输出到屏幕                                           
                perr_exit("write error");                                                               
            for(i = 0 ;i < len ; i++)   //进行大写转换                                                  
                buf[i] = toupper(buf[i]);                                                               
            if(write(cfd,buf,len) < 0) //写数据到客户端                                                 
                perr_exit("write error");                                                               
        }                                                                                               
        //关闭打开的文件描述符,虽然不会执行到这里往下的部分,但要养成良好习惯,打开文件描述符,用完    
        //就要关闭                                                                                      
        close(sfd);                                                                                     
        close(cfd);                                                                                     
        return 0;                                                                                       
    }         

    内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。
    (可以用uion去测试本机是小端还是大端,这里就不去讨论了,有兴趣可以百度一下)

    网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?
    发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,
    接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此网络数据流的地址应这样规定:
    先发出的数据是低地址,后发出的数据是高地址。

    TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
    但是在主机中一般是用小端存储(高地址低字节),这就不可避免会遇到数据解释错误的问题。
    为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

    #include <arpa/inet.h>
    
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);
    h表示host,n表示network,l表示32位长整数,s表示16位短整数。
    如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,
    如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

    服务器程序搞定了,接下来就需要写客户端程序去连接服务器程序了。客户端就不需要bind了,为什么呢, 这是因为由于客户端不需要固定的端口号,这样客户端的端口号由内核自动分配就可以了。(注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。)
    所以在客户端程序上我们就不需要bind()了,而是直接connect()去连接服务器

    5.connect()

    #include <sys/types.h> /* See NOTES */
    #include <sys/socket.h>
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    与前面的bind()的参数类型差不多,返回值所代表的意义也类似,就不继续解释了
    sockdf:
        socket文件描述符
    addr:
        传入参数,指定服务器端地址信息,含IP地址和端口号
    addrlen:
        传入参数,传入sizeof(addr)大小
    返回值:
        成功返回0,失败返回-1,设置errno

    到此,client的基本架构就结束了,等待connect连上服务器后,便可对socket文件描述符进行read,write操作来进行通信,具体代码如下(为了简化逻辑,也没有过多的出错处理)

    /*client.c*/                                                                                        
    #include <sys/types.h>                                                                              
    #include <sys/socket.h>                                                                             
    #include <unistd.h>                                                                                 
    #include <stdio.h>                                                                                  
    #include <stdlib.h>                                                                                 
    #include <strings.h>                                                                                
    #include <string.h>                                                                                 
    #include <arpa/inet.h>                                                                              
    #define SERV_PORT 8000                                                                              
    void perr_exit(const char *str)                                                                     
    {                                                                                                   
        perror(str);                                                                                    
        exit(1);                                                                                        
    }                                                                                                   
    int main(int argc,char *argv[])                                                                     
    {                                                                                                   
        int sfd,len;                                                                                    
        sfd = socket(AF_INET,SOCK_STREAM,0);                                                            
        char buf[256];                                                                                  
        struct sockaddr_in serv_addr;                                                                   
    
        bzero(&serv_addr,sizeof(serv_addr));                                                            
        //init                                                                                          
        serv_addr.sin_family = AF_INET;                                                                 
        serv_addr.sin_port = htons(SERV_PORT);                                                          
        inet_pton(AF_INET,argv[1],&serv_addr.sin_addr.s_addr);//转换char *IP地址为网络二进制序          
    
       if(connect(sfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) < 0)                              
           perr_exit("connect error");                                                                  
        while(fgets(buf,sizeof(buf),stdin))//读取终端输入的数据                                         
        {                                                                                               
            if(write(sfd,buf,strlen(buf)) < 0)//写入数据到服务器                                        
                perr_exit("write error");                                                               
            len = read(sfd,buf,sizeof(buf));//读取服务器传递的数据                                  
            if(len <0)                                                                                  
                perr_exit("read error");
            if(len == 0)
            {
                printf("the other size closed\n");
                close(sfd);
                exit(1);
            }                                                                    
            if(write(STDOUT_FILENO,buf,len) < 0)//输出到终端                                            
                perr_exit("write error");                                                               
        }                                                                                               
        return 0;                                                                                       
    }    

    代码进行编译过后
    运行结果如下
    服务器端在没有客户端连上来时,处于阻塞
    这里写图片描述
    客户端(127.0.0.1代表本地地址)
    这里写图片描述

    此时的服务器端接受到连接请求之后
    这里写图片描述

    通信过程
    1.客户端读取终端输入,并将信息传递给服务器
    2.服务器收到客户端传递过来的数据,输出到终端,并转换成大写,传递给客户端
    3.客户端接受到服务器的数据,输出到终端
    客户端
    这里写图片描述

    服务器端
    这里写图片描述

    进行完简单的通信了,我们就来讨论下其中涉及到的一些网络知识
    服务器调用socket(),bind(),listen()完成初始化后,调用accept()阻塞等待。
    客户端调用socket()完成初始化,发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到从connect()返回,同时应答一个ACK,服务器收到后从accept()返回。
    这样就建立起了链接,这便是TCP建立连接时的三次握手。

    以下为比较官方的解释,参考自书籍
    TCP建立起连接的方式(三次握手)
    1.客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。

    2.服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明mss为1024。

    3.客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。
    在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为’‘’三方握手(three-way-handshake)”’。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。

    数据传输的过程:
    1.客户端发出段4,包含从序号1001开始的20个字节数据。
    2.服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。
    3.客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。

    关闭连接的过程:(4次挥手)
    1.客户端发出段7,FIN位表示关闭连接的请求。
    2.服务器发出段8,应答客户端的关闭连接请求。
    3.服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
    4.客户端发出段10,应答服务器的关闭连接请求。

    这里写图片描述

    我们把上面的图补充完整之后,如下
    这里写图片描述

    CLOSED:
    表示初始状态

    LISTEN:
    表示服务器端的SOCKET处于监听状态

    SYN_SENT:
    可以在字面上进行理解,表示发送SYN报文,当客户端程序调用connect(),它需要先发起连接请求,发送SYN报文,随机便进入该状态。

    SYN_RCVD:
    与上面的类似,表示收到SYN。即这个状态是服务器端在三次握手期间收到客户端发起请求连接的报文SYN。

    ESTABLISHED:
    表示连接已经建立(即3次握手完成)

    FIN_WAIT_1
    表示等待对方FIN报文。与FIN_WAIT2不同的是,当SOCKET主动关闭连接时,向对方发送起结束连接请求FIN报文,此时该SOCKET就处于FIN_WAIT_1状态。

    CLOSE_WAIT
    表示等待关闭,当接受到对方发送FIN报文,系统会对这个FIN报文回应ACK应答,此时便进入到CLOSE_WAIT状态。若此时没有数据需要进行处理,那么就可以关闭这个SOCKET,并发送FIN报文给对方,表示关闭连接

    FIN_WAIT_2
    当处于FIN_WAIT_1的SOCKET收到对方的ACK应答之后,则从FIN_WAIT_1进入FIN_WAIT_2,表示半关闭连接。即有一方要求关闭连接,但另外一方仍有数据需要处理,并要发送数据回去,只能稍后再关闭连接。

    TIME_WAIT
    表示收到了对方的FIN报文,并发送ACK报文,就等2MSL后就可以返回CLOSED状态。在此一提,假设处于FIN_WAIT_1状态下,如果同时收到对方的ACK和FIN报文的话,就直接进入TIME_WAIT状态,而不会经历FIN_WAIT_2状态

    LAST_ACK:即当处于CLOSE_WAIT,发送FIN报文后,在等待对方的ACK报文。当收到ACK报文后,也就可以处理初始化的CLOSED状态

    另外还有图中没有涉及到的状态CLOSING,这种状态比较特殊,以我们的正常理解来说一般都是当你发送FIN报文主动去关闭连接的时候,应该先收到对方的ACK应答,在收到对方的FIN报文。
    而CLOSING状态表明是没收到对方的ACK应答,却收到了对方的FIN报文,那么此时就会处于CLOSING状态,表示双方都正在关闭连接。

    这里我的端口号是8000
    0.0.0.0表示全部网络,指的是全部网络都能连到这台主机上
    对于网络状态的查询我们可以使用命令netstat -apn | grep 8000,依次对应的选项为
    Proto协议 Recv-Q网络接收队列 Send-Q网络发送队列 Local Address Foreign Address State PID/Program name
    这里写图片描述

    如果当我们直接ctrl+c终止掉客户端程序后,立即开启另外一个终端查看时,此时服务器接受到客户端发出的FIN报文后,并发送ACK报文给客户端,此时服务器便处于CLOSE_WAIT状态,但由于还处在while(1)的大循环中,故不会发送FIN报文给客户端,故此时客户端就会处于FIN_WAIT_2状态,而服务器就会处于CLOSE_WAIT状态。
    这里写图片描述
    等过一段时间服务器write数据给客户端之后,客户端就会脱离FIN_WAIT_2状态,而进入CLOSED,而奇怪的是服务器端仍处于CLOSE_WAIT状态,这是因为当read在读取数据时候,在没有数据的时候会返回0,使得,服务器端一直处于while(1)大循环中,无法脱离,这时我们需要优化下程序。(即加上len==0的判断,把我的下面那个注释去掉就可以了)
    这里写图片描述

    当把注释去掉,便是完整的简易TCP 模型,当CTR+C掉一个服务器端或者客户端,相应的另一端都会关闭。
    也许你会遇到 bind error,这可能是因为你服务器绑定的端口被其他进程用了,可以用netstat -apn | grep 端口号查看,或者是前面执行的./server还处于TIME_WAIT状态,要等2MSL时间,才会返回初始状态CLOSED,Linux的话普遍大概1分钟。
    这里写图片描述

    展开全文
  • UNIX网络编程——TCP/IP简介

    千次阅读 2013-08-01 15:57:04
    一、ISO/OSI参考模型OSI(open system interconnection)开放系统互联模型是由ISO(International Organization for Standardization)国际标准化组织定义的网络分层模型,共七层,如下图。物理层(Physical Layer):...

    一、ISO/OSI参考模型

           OSI(open system interconnection)开放系统互联模型是由ISO(International Organization for Standardization)国际标准化组织定义的网络分层模型,共七层,如下图:

                                 

           物理层(Physical Layer):物理层定义了所有电子及物理设备的规范,为上层的传输提供了一个物理介质,本层中数据传输的单位为比特(bit)。属于本层定义的规范有EIA/TIA RS-232、EIA/TIA RS-449、V.35、RJ-45等,实际使用中的设备如网卡等属于本层。


           数据链路层(Data Link Layer):对物理层收到的比特流进行数据成帧。提供可靠的数据传输服务,实现无差错数据传输。在数据链路层中数据的单位为帧(frame)。属于本层定义的规范有SDLC、HDLC、PPP、STP、帧中继等,实际使用中的设备如switch交换机属于本层。


           网络层(Network Layer):网络层负责将各个子网之间的数据进行路由选择,分组与重组。本层中数据传输的单位为数据包(packet)。属于本层定义的规范有IP、IPX、RIP、OSPF、ICMP、IGMP等。实际使用中的设备如路由器属于本层。


           传输层(Transport Layer):提供可靠的数据传输服务,它检测路由器丢弃的包,然后产生一个重传请求,能够将乱序收到的数据包重新排序。在传输层数据的传输单位是段(segment)

           会话层(Session Layer):管理主机之间会话过程,包括会话建立、终止和会话过程中的管理。

           表示层(Presentation Layer):表示层对网络传输的数据进行变换,使得多个主机之间传送的信息能够互相理解,包括数据的压缩、加密、格式转换等。

           应用层(Application Layer):应用层与应用程序界面沟通,以达至展示给用户的目的。 在此常见的协定有: HTTP,HTTPS,FTP,TELNET,SSH,SMTP,POP3等


    二、TCP/IP协议四层模型

           TCP/IP网络协议栈分为应用层(Application)、传输层(Transport)、网络层(Network)和链路层(Link)四层。如下图所示,如果没有特别说明,一般引用的图都出自《TCP/IP详解》。

                                                           

           两台计算机通过TCP/IP协议通讯的过程如下所示:

                                                    

           传输层及其以下的机制由内核提供,应用层由用户进程提供,应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示:

                                       

           不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。
           上图对应两台计算机在同一网段中的情况,如果两台计算机在不同的网段中,那么数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器,如下图所示:

                                                    

           其实在链路层之下还有物理层,指的是电信号的传递方式,比如现在以太网通用的网线(双绞线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)是工作在物理层的网络设备,用于双绞线的连接和信号中继(将已衰减的信号再次放大使之传得更远)。

           链路层有以太网、令牌环网等标准,链路层负责网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。交换机是工作在链路层的网络设备,可以在不同的链路层网络之间转发数据帧(比如十兆以太网和百兆以太网之间、以太网和令牌环网之间),由于不同链路层的帧格式不同,交换机要将进来的数据包拆掉链路层首部重新封装之后再转发。

           网络层的IP协议是构成Internet的基础。Internet上的主机通过IP地址来标识,Internet上有大量路由器负责根据IP地址选择合适的路径转发数据包,数据包从Internet上的源主机到目的主机往往要经过十多个路由器。路由器是工作在第三层的网络设备,同时兼有交换机的功能,可以在不同的链路层接口之间转发数据包,因此路由器需要将进来的数据包拆掉网络层和链路层两层首部并重新封装。IP协议不保证传输的可靠性,数据包在传输过程中可能丢失,可靠性可以在上层协议或应用程序中提供支持。


           网络层负责点到点(point-to-point)的传输(这里的“点”指主机或路由器),而传输层负责端到端(end-to-end)的传输(这里的“端”指源主机和目的主机)。传输层可选择TCP或UDP协议。TCP是一种面向连接的、可靠的协议,有点像打电话,双方拿起电话互通身份之后就建立了连接,然后说话就行了,这边说的话那边保证听得到,并且是按说话的顺序听到的,说完话挂机断开连接。也就是说TCP传输的双方需要首先建立连接,之后由TCP协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的总是可靠的数据流,通讯之后关闭连接。UDP协议不面向连接,也不保证可靠性,有点像寄信,写好信放到邮筒里,既不能保证信件在邮递过程中不会丢失,也不能保证信件是按顺序寄到目的地的。使用UDP协议的应用程序需要自己完成丢包重发、消息排序等工作。


           目的主机收到数据包后,如何经过各层协议栈最后到达应用程序呢?整个过程如下图所示:

                                     

           以太网驱动程序首先根据以太网首部中的“上层协议”字段确定该数据帧的有效载荷(payload,指除去协议首部之外实际传输的数据)是IP、ARP还是RARP协议的数据报,然后交给相应的协议处理。假如是IP数据报,IP协议再根据IP首部中的“上层协议”字段确定该数据报的有效载荷是TCP、UDP、ICMP还是IGMP,然后交给相应的协议处理。假如是TCP段或UDP段,TCP或UDP协议再根据TCP首部或UDP首部的“端口号”字段确定应该将应用层数据交给哪个用户进程。IP地址是标识网络中不同主机的地址,而端口号就是同一台主机上标识不同进程的地址,IP地址和端口号合起来标识网络中唯一的进程


           注意,虽然IP、ARP和RARP数据报都需要以太网驱动程序来封装成帧,但是从功能上划分,ARP和RARP属于链路层,IP属于网络层。虽然ICMP、IGMP、TCP、UDP的数据都需要IP协议来封装成数据报,但是从功能上划分,ICMP、IGMP与IP同属于网络层,TCP和UDP属于传输层。


    展开全文
  • 1.点对点聊天主要实现一个客户端与一个服务器端的通讯模型。 2.点对点聊天的实现需要服务器和客户端都得有至少两个进程来实现,一个用来发送给对方,一个用来接收对方发送的消息。 3.设计阶段需要考虑当进程结束了...
  • unix网络编程 学习笔记(精华)

    千次阅读 2017-05-26 15:33:13
    网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端.  网络程序是先有服务器程序启动,等待客户端的程序运行并建立连接.一般的来说是服务端的程序 在一个端口上监听,直到有一...
  • 网络通信模型 --select

    千次阅读 2015-08-13 17:17:09
    另外,本文所提及到的接口也只是笔者熟悉的 Unix/Linux 接口,并未推荐 Windows 接口,读者可以自行查阅对应的 Windows 接口。 阻塞型的网络编程接口 几乎所有的程序员第一次接触到的网络编程都是从 listen()...
  • Unix和Windows跨系统通讯编程

    千次阅读 2005-12-27 12:57:00
    本文介绍了套接字(Socket)的基本概念及编程技术,并结合实例说明在Unix和Windows下如何用套接字实现客户/服务器方式的通讯编程。摘 要 本文介绍了套接字(Socket)的基本概念及编程技术,并结合实例说明在Unix和...
  • UNIX网络编程

    千次阅读 2006-06-11 15:06:00
    UNIX网络编程[size=16]UNIX网络编程1.1 客户端程序和服务端程序 网络程序和普通的程序有一个最大的区别是网络程序是由两个部分组成的--客户端和服务器端. 网络程序是先有服务器程序启动,等待客户端的程序运行并建立...
  • 全书共31章+附录。 计划安排:吃透这本书,一天三章+源码,并实测代码做当天笔记,CSDN见。 时间安排:计划时间1.5个月 == 6个周末 == 12个自然日。 2017.08.05 第01-03章:TCP/IP简介、传输层、套接字编程简介 ...
  • TCP/IP协议四层模型

    万次阅读 多人点赞 2017-11-18 18:14:03
    参照的书籍有《Linux高性能服务器编程》(游双著)、《UNIX网络编程-卷1:套接字联网API》。  TCP/IP协议族是一个四层协议系统:1. 数据链路层  1.1 作用   (1) 实现网卡接口的网络驱动,以处理数据在以太网线...
  • 朴素的UNIX之-翻开历史

    千次阅读 2014-10-18 09:33:50
    可以毫不夸张地说,UNIX模型就是现代操作系统的原型!不管是原汁原味的UNIX各大系列比如AIX,Solaris,HP-UX,FreeBSD,NetBSD,...还是类UNIX比如Linux...还是基于Windows NT架构的各种微软操作系统,其基本思想都...
  • UNIX环境高级编程——网络基础概念

    千次阅读 2013-07-19 13:23:12
    TCP协议分成两个不同的协议:1、网络传输中差错的传输控制协议TCP2、专门负责对不同网络进行互联的互联网协议IP网络体系结构概念:网络体系结构即是指网络的层次结构和每层所使用协议的集合OSI:(Open System ...
  • 关于 unix socket 的概念与应用 socket通信有以下二种, 1、Internet domain socket 基于网络协议栈的,是网络中不同主机之间的通讯,需要明确IP和端口。 2、unix domain socket 同一台主机内不同应用不同进程间的...
  • unix网络编程之一简述

    2011-12-05 10:36:55
    如今大部分网络应用为C/S(客户端和服务器)模式或B/S(浏览器/服务器)模式。如:ftp客户端和ftp服务器。telnet客户端和telnet服务器。web浏览器和web服务器。而这之间通讯就是通过各种协议簇来实现的。大多数使用TCP...
  • 服务器设计系列:网络模型

    千次阅读 2013-11-13 22:29:54
    全文针对linux环境。...先从基本说起,看一个单线程的网络模型,处理流程如下:  socket-->bind-->listen-->[accept-->read-->write-->close]-->close  []中代码循环运行,[]外的是对监听socket的处理,[]内的是对
  • 多线程服务器通讯模型

    千次阅读 2006-06-19 00:34:00
    通用的 socket 服务器处理端,也就是进程所在的主线程每次接收一个连接后,处理完毕后,在接收一个连接,周而复始,完成和客户端的通讯过程。缺点是显而意见的,特别是处理客户端的长连接的时候,服务器和客户端的...
  • [Unix下C/C++开发] unix编程书籍推荐

    千次阅读 2014-02-23 15:23:06
    [Unix下C/C++开发] unix编程书籍推荐 发表于1年前(2012-12-20 10:14) 阅读(256) | 评论(0) 6人收藏此文章, 我要收藏 赞0 Unix/Linux/BSD系统  相对于Windows,在UNIX下编程获得相关文档要...
  • 事件驱动为广大的程序员所熟悉,其最为人津津乐道的是在图形化界面编程中的应用;事实上,在网络编程中事件驱动也被广泛使用...关于本文提及的服务器模型,搜索网络可以查阅到很多的实现代码,所以,本文将不拘泥于源代
1 2 3 4 5 ... 20
收藏数 17,480
精华内容 6,992
关键字:

unix 网络通讯模型