zoukankan      html  css  js  c++  java
  • Linux之Socket编程

    1.什么是Socket?

    socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),socket就提供了这些操作对应的函数接口。

    socket可以看成是用户进程与内核网络协议栈的编程接口。

    socket不仅可以用于本机的进程间通信,还可以 用于网络上不同主机的进程间通信。

    image

    2.IPv4套接口地址结构

    IPV4套妾口地址结构通常也弥为“网际套接字地址结构”,它以 “sockaddr_in”命名,定义在头文件<netinet/in.h>中

    struct sockaddr_in{
        uint8_t sin_len;    //整个sockaddr_in结构体的长度
        sa_family_t sin_family;    //指定该地址家族,在这里必须设为AF_INET
        in_port_t sin_port;    //端口(2字节)
        struct in_addr sin_addr;    //IPv4的地址(4字节)
        char sin_zero[8];     //暂不使用,一般将其设置为0 (8字节)
    }

    IPv4套接字一般只需关心3个字段

    struct sockaddr_in {
        sa familytsin_family;/* address family:AF_INET */
        in_portt sin_port;/* port in network byte order */
        struct in_addr sin_addr;/* internet address */ 
    }; /* Internet address.*/ 
    
    struct in_addr {
        uint32_t s_addr;/* address in hetwork byte order
    }

    3.通用地址结构

    通用地址结构用来指定与套接字关联的地址

    struct sockaddr{
        uint8_t sin_len;    //整个sockaddr结构体的关度
        sa_ family_t sin_family;    //指定该地址家族
        char sa_data[14];    //由sin_family决定它的形式
    }

    因为不同的协议地址结构形式可能不一样,通用的可以用于任何协议的接口

    一般将IPv4的sockaddr_in强行转换为通用的地址结构sockaddr

    4.网络字节序

    字节序 

        大端字节序(Big Endian) :最高有效位(MSB:Most Significant Bit)存储于最低内存地址 处,最低有效位(LSB:Lowest Significant Bit;存储于最高内存地坨处。

        小端字节序(l.ittle ndian) :最高有效位(MSB:Most Significant Bit)存储于最高内存地址 处,最低有效位(LSB:Lowest Significant Bit>存储于最低内存地垃处。

    主机字节序

        不同的主有不同的字节序,如:86为小端字节序,Motorola 6800为 大端字节序,ARM字节序是可配置的。

    网络字节序

        网络字节序规定为大端字节序

    5c4ecf69-2757-4aa6-9301-679ccc1db48f

    为了将字节序统一,就出现了网络字节序,为大端字节序

    编写一个程序测试大小端 

    #include<stdio.h>
    int main(void)
    {
        unsigned int x=0x12345678;
        unsigned char *p=(unsigned char*)&x;
        printf("%0x %0x %0x %0x
    ",p[0],p[1],p[2],p[3]);
        return 0;
    }

    Linux下运行结果:

    a645325a-b6a9-42ee-94e1-aad0f38c895c

    说明是小端模式

    5.字节序的转换函数

    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

    s代表 short

    I代表 long

    程序测试网络字节序

    #include<stdio.h>
    #include<arpa/inet.h>
    int main()
    {
        unsigned int x = 0x12345678;
        unsigned int y=htonl(x);
        unsigned char *p=(unsigned char*)&y;
        printf("%0x %0x %0x %0x
    ",p[0],p[1],p[2],p[3]);
        return 0;
    }

    运行结果:

    0a315c5b-ec69-4bf3-ac38-41ea5eb18b15

    可见,网络字节序会将本地字节序转换为大端模式

    6.地址转换函数

    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    int inet_ aton(const char *cp,struct in_addr *inp);//将点分十进制转换为网络字节序的结构
    in_addr_t inet_addr(const char *cp);    //将点分十进制IP地址转换为32位整数
    char *inet_ntoa(struct in_addr in);    //将网络字节序的结构转换为点分十进制

    一般的地址:点分十进制形式,如:192.168.0.100

    测试程序:

    int main()
    {
        unsigned long addr=inet_addr("192.168.0.100");
        cout<<ntohl(addr)<<endl;
        return 0;
    }
    //运行结果:3232235620
    int main()
    {
        unsigned long addr=inet_addr("192.168.0.100");
        struct in_addr ipaddr;
        ipaddr.s_addr=addr;
        cout<<inet_ntoa(ipaddr)<<endl;
        return 0;
    }
    //运行结果:192.168.0.100

    7.套接字类型

    常用的三种:

    流式套接字(SOCK_STREAM)  (TCP协议)

    提供面向连接的、可靠的数据传输服务,数据无 差错,无重复的发送,且按发送顺序接收。

    数据报式套接字(SOCK_DGRAM) (UDP协议)

    提供无连接服务。不提供无错保证,数据可能丢 失或重复,并且接收顺序混乱。

    原始套接字(SOCK_RAW)

    可以将应用层直接封装成IP层能认识的协议格式

    8.TCP客户/服务器模型

    418fd341-7f58-4ab7-bbe4-c12704a911b8

    9.回射客户/服务器

    6da5d374-af3d-4cc9-93a1-810c402994fd

    10.socket的基本函数


    socket函数

    头文件

    <sys/socket.h>

    功能:创建一个套接字用于通信 

    原型 

    int socket(int domain,int type,int protocol);

    参数 

    domain:指定通信协议族(protocol family)

           Name                Purpose                          Man page
           AF_UNIX, AF_LOCAL   Local communication              unix(7)
           AF_INET             IPv4 Internet protocols          ip(7)
           AF_INET6            IPv6 Internet protocols          ipv6(7)
           AF_IPX              IPX - Novell protocols
           AF_NETLINK          Kernel user interface device     netlink(7)
           AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
           AF_AX25             Amateur radio AX.25 protocol
           AF_ATMPVC           Access to raw ATM PVCs
           AF_APPLETALK        Appletalk                        ddp(7)
           AF_PACKET           Low level packet interface       packet(7)

    type:指定socket类型,流式套接字SOCK_STREAM,数据报套 接字SOCK DGRAM,原始套接字SOCK RAW

           SOCK_STREAM     Provides  sequenced,  reliable,  two-way,  connection-based
                           byte  streams.   An out-of-band data transmission mechanism
                           may be supported.
    
           SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages  of
                           a fixed maximum length).
    
           SOCK_SEQPACKET  Provides  a  sequenced,  reliable, two-way connection-based
                           data transmission  path  for  datagrams  of  fixed  maximum
                           length;  a  consumer  is  required to read an entire packet
                           with each input system call.
    
           SOCK_RAW        Provides raw network protocol access.
    
           SOCK_RDM        Provides a reliable datagram layer that does not  guarantee
                           ordering.
    
           SOCK_PACKET     Obsolete  and  should  not  be  used  in  new programs; see
                           packet(7).
           SOCK_NONBLOCK   Set  the  O_NONBLOCK  file status flag on the new open file
                           description.  Using this flag saves extra calls to fcntl(2)
                           to achieve the same result.
    
           SOCK_CLOEXEC    Set  the  close-on-exec  (FD_CLOEXEC)  flag on the new file
                           descriptor.  See the description of the O_CLOEXEC  flag  in
                           open(2) for reasons why this may be useful.

    protocol:协议类型 

    返回值:成功返回非负整数,它与文件描述符类似,我们把它称为套接口描述字,简称套接字。失败返回-1

    bind函数

    功能:绑定一个本地地址到套接字

    原型

    int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);

    参数

    sockfd:socket函数返回的套接字

    addr:要绑定的地址

    addrlen:地址长度

    返回值:成功返回0,失败返回-1

    listen函数

    功能:将套接字用于监听进入的连接

    原型

    int listen (int sockfd,int backlog);

    参数

    sockfd: socket函数返回的套接字口

    backlog: 规定内核为此套接字排队的最大连妾个数口,表示已完成队列和未完成队列的组合,未完成队列表示三次握手还没有成功的条目

    返回值:成功返回0,失败返回-1,规定了并发连接的数目

    一般来说,listen函数应该在调用socket和bind函数之后,调用函数accept之前调用。

    对于给定的监听套接口,内核要维护两个队列:

    1、已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程

    2、已完成连接的队列

    // 调用listen函数后,就成了被动套接字,否则是主动套接字

    // 主动套接字:发送连接(connect)

    // 被动套接字:接收连接(accept)

    384ad278-1aac-4d74-81b2-a8a6078a0845

    accept函数

    功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。

    原型

    int accept(int sockfd,struct sockaddr*addr,socklen_t*addrlen);

    sockfd:服务器套接字

    addr:将返回对等方的套接字地址,相当于把对方的信息填充到结构体中

    addrlen:返回对等方的套接字地址长度

    返回值:成功返回非负整数,失败返回-1

    connect函数

    功能:建立一个连接至addr所指定的套接字

    原型

    int connect(int sockfd;const struct sockaddr*addr;socklen_t addrlen)

    参数

    sockfd:未连接套接字

    addr:要连接的套接字地址

    addrlen:第二个参数addr长度

    返回值:成功返回0,失败返回-1

    发送/接收函数

    服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作,即实现了网咯中不同进程之间的通信.网络I/O操作有下面几组:

    #include <unistd.h>
    
    ssize_t read(int fd, void *buf, size_t count);
    ssize_t write(int fd, const void *buf, size_t count);
    
    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    
    ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
    ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

    推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。

    close()函数

    在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

    #include <unistd.h>
    int close(int fd);

    close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

    注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。


    11.简单的服务器客户端程序

    回射服务器代码

    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    #include<stdlib.h>
    #include<error.h>
    #include<string.h>
    #include<iostream>
    using namespace std;
    
    #define ERR_EXIT(m) 
            do
            {
                perror(m);
                exit(EXIT_FAILURE);
            } while (0);
    
    int main(void)
    {
        //socket
        int listenfd;
        //listenfd=socket(PF_INET,SOCK_STREAM,0);
        if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
        {
            ERR_EXIT("socket");
        }
    
        //填充地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family=AF_INET;
        servaddr.sin_port=htons(5188);
        servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        //servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
        //inet_aton("127.0.0.1",&servaddr.sin_addr);
    
        //地址复用
        int on=1;
        if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
        {
            ERR_EXIT("setsocketopt");
        }
    
        //bind 绑定listenfd和本地地址结构
        if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
        {
            ERR_EXIT("bind");
        }
    
        if(listen(listenfd,SOMAXCONN)<0)
        {
            ERR_EXIT("listen");
        }
    
        // 调用listen函数后,就成了被动套接字,否则是主动套接字
        // 主动套接字:发送连接(connect)
        // 被动套接字:接收连接(accept)
    
        //对方的地址
        struct sockaddr_in peeraddr;
        socklen_t peerlen=sizeof(peeraddr);
        int conn;   //已连接套接字(主动)
        if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
        {
            ERR_EXIT("accept");
        }
        //连接成功后打印客户端的ip和端口
        printf("client: ip=%s | port=%d
    ",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
    
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
    
            int ret=read(conn,recvbuf,sizeof(recvbuf));
            fputs(recvbuf,stdout);
            write(conn,recvbuf,ret);
        }
    
        //关闭套接口
        close(conn);
        close(listenfd);
        return 0;
    }

    回射客户端代码

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <error.h>
    #include <string.h>
    
    #define ERR_EXIT(m)         
        do                      
        {                       
            perror(m);          
            exit(EXIT_FAILURE); 
        } while (0);
    
    int main()
    {
        //socket
        int sock;
        if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
        {
            ERR_EXIT("socket");
        }
    
        struct sockaddr_in cliaddr;
        memset(&cliaddr, 0, sizeof(cliaddr));
        cliaddr.sin_family = AF_INET;
        cliaddr.sin_port = htons(2019);
        cliaddr.sin_addr.s_addr = htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        if(bind(sock,(struct sockaddr*)&cliaddr,sizeof(cliaddr))<0)
        {
            ERR_EXIT("bind");
        }
    
        //指定服务器的地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family=AF_INET;
        servaddr.sin_port=htons(5188);
        servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    
        //客户端不需要绑定和监听
        //connect 用本地套接字连接服务器的地址
        if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
            ERR_EXIT("connect");
    
        char sendbuf[1024]={0};
        char recvbuf[1024]={0};
        while (fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
        {
            write(sock,sendbuf,strlen(sendbuf));
            read(sock,recvbuf,sizeof(recvbuf));
            fputs(recvbuf,stdout);
            memset(sendbuf,0,sizeof(sendbuf));
            memset(recvbuf,0,sizeof(recvbuf));
        }
    
        //关闭套接字
        close(sock);
    
        return 0;
    }

    5ccfff87-51ca-41d3-bcd8-dbc19dc4b248

    客户端的套接字sock和服务器的套接字conn构成连接,两个套接字都有自己的地址

    conn是在绑定的时候确定的

    sock实在连接成功的时候确定的


    12.为客户端绑定端口

    客户端一般不需要绑定端口,如果不绑定,一般是随机的端口,但是也可以为其绑定固定的端口

    //socket
        int sock;
        if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
        {
            ERR_EXIT("socket");
        }
    
        //指定客户端的地址结构
        struct sockaddr_in cliaddr;
        memset(&cliaddr, 0, sizeof(cliaddr));
        cliaddr.sin_family = AF_INET;
        cliaddr.sin_port = htons(2019);
        cliaddr.sin_addr.s_addr = htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        if(bind(sock,(struct sockaddr*)&cliaddr,sizeof(cliaddr))<0)
        {
            ERR_EXIT("bind");
        }
    
        //指定服务器的地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family=AF_INET;
        servaddr.sin_port=htons(5188);
        servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    
        //客户端不需要绑定和监听
        //connect 用本地套接字连接服务器的地址
        if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
            ERR_EXIT("connect");

    在服务器端接收客户端后,打印出客户端的端口

    if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
        {
            ERR_EXIT("accept");
        }
        //连接成功后打印客户端的ip和端口
        printf("client: ip=%s | port=%d
    ",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));

    结果是:

    74de35c1-0b1d-4b43-aea5-a2449ca804bc

    13.地址复用REUSEADDR

    ea4ef387-86ee-464c-bfca-9b6c48c8287c

    服务器在重启后,在重新运行时绑定会报地址已经使用,因为现在服务器处于TIME_WAIT状态

    这个状态下,不能再绑定客户端,需要等待一段时间。

    解决办法:地址复用

    服务器端尽可能使用REUSEADDR

    绑定之前尽可能调用setsockopt来设置REUSEADDR套接字选项。

    使用REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器。

    使用REUSEADDR可以在TIME_WAIT状态还没有消失的时候,就允许重启

    //地址复用
        int on=1;
        if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
        {
            ERR_EXIT("setsocketopt");
        }
    
        //bind 绑定listenfd和本地地址结构
        if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
        {
            ERR_EXIT("bind");
        }

    14.简单的多进程服务器连接多个客户端

    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    #include<stdlib.h>
    #include<error.h>
    #include<string.h>
    #include<iostream>
    using namespace std;
    //////////////////////////////////////////
    #define ERR_EXIT(m) 
            do
            {
                perror(m);
                exit(EXIT_FAILURE);
            } while (0);
    
    void do_service(int conn)
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if(ret==0)  //客户端关闭了
            {
                printf("client_close!");
                break;
            }
            else if(ret==-1)
            {
                ERR_EXIT("read");
            }
            fputs(recvbuf, stdout);
            write(conn, recvbuf, ret);
        }
    }
    
    ///////////////////////////////////////////////////
    
    int main(void)
    {
        //socket
        int listenfd;
        //listenfd=socket(PF_INET,SOCK_STREAM,0);
        if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
        {
            ERR_EXIT("socket");
        }
    
        //填充地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family=AF_INET;
        servaddr.sin_port=htons(5188);
        servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        //servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
        //inet_aton("127.0.0.1",&servaddr.sin_addr);
    
        //地址复用
        int on=1;
        if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
        {
            ERR_EXIT("setsocketopt");
        }
    
        //bind 绑定listenfd和本地地址结构
        if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
        {
            ERR_EXIT("bind");
        }
    
        if(listen(listenfd,SOMAXCONN)<0)
        {
            ERR_EXIT("listen");
        }
    
        // 调用listen函数后,就成了被动套接字,否则是主动套接字
        // 主动套接字:发送连接(connect)
        // 被动套接字:接收连接(accept)
    
        //对方的地址
        struct sockaddr_in peeraddr;
        socklen_t peerlen=sizeof(peeraddr);
        int conn;   //已连接套接字(主动)
    
        pid_t pid;
        while (1)
        {
            if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
            {
                ERR_EXIT("accept");
            }
            //连接成功后打印客户端的ip和端口
            printf("client: ip=%s | port=%d
    ",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
    
            pid=fork();
            if(pid==-1)
            {
                ERR_EXIT("fork");
            }
            if (pid==0)
            {
                /* 子进程的处理 */
                close(listenfd);    //子进程不需要处理监听,子进程处理通信细节
                //通信处理封装函数
                do_service(conn);
                //一旦客户端关闭进程返回了,这个子进程就要结束
                exit(EXIT_SUCCESS);
            }
            else
            {
                /*父进程的处理 */
                close(conn);    //父进程不需要处理连接
            }
        }
        return 0;
    }

    一个连接对应一个进程来并发处理

    a88168d8-3a42-43a3-8cae-73910258acaa

    服务器有两种套接字,

    1.监听套接字:处理三次握手,一旦三次握手创建完成,就将其放在已连接队列中,accept就可以从队列中返回一个连接

    2.已连接套接字:accept返回的套接字,主要用来通信,并不能用来接受连接

    15.使用多进程实现点对点的聊天程序

    服务器

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <error.h>
    #include <string.h>
    #include <iostream>
    #include<signal.h>
    
    using namespace std;
    
    #define ERR_EXIT(m)         
        do                      
        {                       
            perror(m);          
            exit(EXIT_FAILURE); 
        } while (0);
    
    //信号处理函数
    void handler(int sig)
    {
        printf("recv a sig=%d
    ",sig);
        exit(EXIT_SUCCESS);
    }
    
    
    int main(void)
    {
        //socket
        int listenfd;
        //listenfd=socket(PF_INET,SOCK_STREAM,0);
        if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        {
            ERR_EXIT("socket");
        }
    
        //填充地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        //servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
        //inet_aton("127.0.0.1",&servaddr.sin_addr);
    
        //地址复用
        int on = 1;
        if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        {
            ERR_EXIT("setsocketopt");
        }
    
        //bind 绑定listenfd和本地地址结构
        if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        {
            ERR_EXIT("bind");
        }
    
        if (listen(listenfd, SOMAXCONN) < 0)
        {
            ERR_EXIT("listen");
        }
    
        // 调用listen函数后,就成了被动套接字,否则是主动套接字
        // 主动套接字:发送连接(connect)
        // 被动套接字:接收连接(accept)
    
        //对方的地址
        struct sockaddr_in peeraddr;
        socklen_t peerlen = sizeof(peeraddr);
        int conn; //已连接套接字(主动)
        if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
        {
            ERR_EXIT("accept");
        }
        //连接成功后打印客户端的ip和端口
        printf("client: ip=%s | port=%d
    ", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
    
        pid_t pid;
        pid=fork();
        if(pid==-1)
        {
            ERR_EXIT("fork");
        }
        if(pid==0)  //子进程,发送数据
        {
            signal(SIGUSR1,handler);    //handler是受到信号后的处理函数
            char sendbuf[1024];
            while (fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
            {
                write(conn,sendbuf,strlen(sendbuf));
                memset(sendbuf,0,sizeof(sendbuf));
            }
            exit(EXIT_SUCCESS);
        }
        else    //父进程,接受数据
        {
            char recvbuf[1024];
            while (1)
            {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(conn, recvbuf, sizeof(recvbuf));
                if(ret==-1) //读取失败
                {
                    ERR_EXIT("read");
                }
                if(ret==0)  //对方关闭
                {
                    printf ("peer close
    ");
                    break;
                }
                fputs(recvbuf, stdout);
            }
            //父进程退出时,向子进程发送kill信号
            kill(pid,SIGUSR1);  //父进程得到的pid是子进程的pid,子进程得到的pid为0
            exit(EXIT_SUCCESS);
        }
        return 0;
    }

    客户端

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <error.h>
    #include <string.h>
    #include<signal.h>
    
    #define ERR_EXIT(m)         
        do                      
        {                       
            perror(m);          
            exit(EXIT_FAILURE); 
        } while (0);
    
    //信号处理函数
    void handler(int sig)
    {
        printf("recv a sig=%d
    ", sig);
        exit(EXIT_SUCCESS);
    }
    
    int main()
    {
        //socket
        int sock;
        if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        {
            ERR_EXIT("socket");
        }
    
        // struct sockaddr_in cliaddr;
        // memset(&cliaddr, 0, sizeof(cliaddr));
        // cliaddr.sin_family = AF_INET;
        // cliaddr.sin_port = htons(2019);
        // cliaddr.sin_addr.s_addr = htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        // if (bind(sock, (struct sockaddr *)&cliaddr, sizeof(cliaddr)) < 0)
        // {
        //     ERR_EXIT("bind");
        // }
    
        //指定服务器的地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
        //客户端不需要绑定和监听
        //connect 用本地套接字连接服务器的地址
        if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("connect");
    
        pid_t pid;
        pid=fork();
        if(pid==-1)
        {
            ERR_EXIT("fork");
        }
        if (pid==0) //子进程,接受数据
        {
            char recvbuf[1024];
            while (1)
            {
                memset(recvbuf,0,sizeof(recvbuf));
                int ret=read(sock,recvbuf,sizeof(recvbuf));
                if(ret==-1)
                {
                    ERR_EXIT("read");
                }
                else if(ret==0)
                {
                    printf("peer close
    ");
                    break;
                }
                fputs(recvbuf,stdout);
            }
            close(sock);
            kill(getppid(),SIGUSR1);    //杀掉父进程
        }
        else    //父进程,发送数据
        {
            signal(SIGUSR1,handler);
            char sendbuf[1024]={0};
            while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
            {
                write(sock, sendbuf, strlen(sendbuf));
                memset(sendbuf, 0, sizeof(sendbuf));
            }
            close(sock);
        }
        return 0;
    }

    16.socket中TCP的三次握手建立连接详解

    我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

    • 客户端向服务器发送一个SYN J
    • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
    • 客户端再想服务器发一个确认ACK K+1

    只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

    0.6625781949591161

    从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

    总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。

    17.socket中TCP的四次挥手释放连接详解

    上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次挥手释放连接的过程,请看下图:

    0.6960954522564762

    图示过程如下:

    • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
    • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
    • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
    • 接收到这个FIN的源发送端TCP对它进行确认。

    这样每个方向上都有一个FIN和ACK。

    18.流协议与粘包

    TCP 字节流,无边界,对于对等方来说,不能保证一次读操作返回的是一个消息还是多个消息,存在粘包问题

    UDP 报文消息,有边界,能保证对等方,一次读操作返回的是一个消息

    a37dcf33-f2d0-4e0c-abb1-939271e7afdc[5]

    粘包产生的原因

    8dc59776-efd0-48a4-8bc3-67853df9beac

    1.应用层缓冲区大小超过了套接口发送的缓冲区,消息被分隔

    2.TCP传输有最大MSS的限制,可能产生分隔

    3.如果传输的大小超过了MTU的限制,会在ip层进行分割

    4.其他:流量控制,拥塞控制等

    19.readn/writen函数的封装

    readn

    ssize_t readn(int fd, void *buf, size_t count)
    {
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nread = read(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nread == 0)
                return count - nleft;
    
            bufp += nread;
            nleft -= nread;
        }
    
        return count;
    }

    writen

    ssize_t writen(int fd, const void *buf, size_t count)
    {
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nwritten = write(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nwritten == 0)
                continue;
    
            bufp += nwritten;
            nleft -= nwritten;
        }
    
        return count;
    }

    readn和wirten都是以定长包的形式发送,但是不一定每次都要发送的实际数据报这么长的字节,就增加了网络的负担

    改进:可以自定义网络协议的包,定义一个包结构体,存储数据的长度和数据

    19.解决粘包问题

    本质上是要在应用层维护消息与消息的边界

    定长包

    包尾加 (ftp)(本来就有,就无法区分)

    包头加上包体长度(先接受包头的长度,再根据包头接受包体的长度)

    更复杂的应用层协议

    20.改进后的回射客户/服务器程序1(使用存储长度和数据的结构体):

    服务器

    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netinet/in.h>
    #include<arpa/inet.h>
    #include<stdlib.h>
    #include<error.h>
    #include<string.h>
    #include<errno.h>
    
    #include<iostream>
    using namespace std;
    //////////////////////////////////////////
    #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)
    {
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nread = read(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nread == 0)
                return count - nleft;
    
            bufp += nread;
            nleft -= nread;
        }
    
        return count;
    }
    
    ssize_t writen(int fd, const void *buf, size_t count)
    {
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nwritten = write(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nwritten == 0)
                continue;
    
            bufp += nwritten;
            nleft -= nwritten;
        }
    
        return count;
    }
    
    void do_service(int conn)
    {
        struct packet recvbuf;
        int n;  //包的长度
        while (1)
        {
            memset(&recvbuf, 0, sizeof(recvbuf));
            int ret = readn(conn, &recvbuf.len, 4);  //先接受4个字节
            if (ret == -1)
            {
                ERR_EXIT("read");
            }
            if (ret <4) //客户端关闭了
            {
                printf("client_close!");
                break;
            }
            n = htonl(recvbuf.len);
            ret = readn(conn, recvbuf.buf, n);
            if (ret == -1)
            {
                ERR_EXIT("read");
            }
            if (ret<n) //客户端关闭了
            {
                printf("client_close!");
                break;
            }
    
            fputs(recvbuf.buf, stdout);
            writen(conn, &recvbuf, 4+n);
        }
    }
    ///////////////////////////////////////////////////
    
    int main(void)
    {
        //socket
        int listenfd;
        //listenfd=socket(PF_INET,SOCK_STREAM,0);
        if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
        {
            ERR_EXIT("socket");
        }
    
        //填充地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family=AF_INET;
        servaddr.sin_port=htons(5188);
        servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        //servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
        //inet_aton("127.0.0.1",&servaddr.sin_addr);
    
        //地址复用
        int on=1;
        if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
        {
            ERR_EXIT("setsocketopt");
        }
    
        //bind 绑定listenfd和本地地址结构
        if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
        {
            ERR_EXIT("bind");
        }
    
        if(listen(listenfd,SOMAXCONN)<0)
        {
            ERR_EXIT("listen");
        }
    
        // 调用listen函数后,就成了被动套接字,否则是主动套接字
        // 主动套接字:发送连接(connect)
        // 被动套接字:接收连接(accept)
    
        //对方的地址
        struct sockaddr_in peeraddr;
        socklen_t peerlen=sizeof(peeraddr);
        int conn;   //已连接套接字(主动)
    
        pid_t pid;
        while (1)
        {
            if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
            {
                ERR_EXIT("accept");
            }
            //连接成功后打印客户端的ip和端口
            printf("client: ip=%s | port=%d
    ",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
    
            pid=fork();
            if(pid==-1)
            {
                ERR_EXIT("fork");
            }
            if (pid==0)
            {
                /* 子进程的处理 */
                close(listenfd);    //子进程不需要处理监听,子进程处理通信细节
                //通信处理封装函数
                do_service(conn);
                //一旦客户端关闭进程返回了,这个子进程就要结束
                exit(EXIT_SUCCESS);
            }
            else
            {
                /*父进程的处理 */
                close(conn);    //父进程不需要处理连接
            }
        }
        return 0;
    }

    客户端

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <error.h>
    #include <string.h>
    #include<errno.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)
    {
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nread = read(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nread == 0)
                return count - nleft;
    
            bufp += nread;
            nleft -= nread;
        }
    
        return count;
    }
    
    ssize_t writen(int fd, const void *buf, size_t count)
    {
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nwritten = write(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nwritten == 0)
                continue;
    
            bufp += nwritten;
            nleft -= nwritten;
        }
    
        return count;
    }
    
    /////////////////////////////////////////////////////////////////
    int main()
    {
        //socket
        int sock;
        if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
        {
            ERR_EXIT("socket");
        }
    
        // struct sockaddr_in cliaddr;
        // memset(&cliaddr, 0, sizeof(cliaddr));
        // cliaddr.sin_family = AF_INET;
        // cliaddr.sin_port = htons(2019);
        // cliaddr.sin_addr.s_addr = htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        // if(bind(sock,(struct sockaddr*)&cliaddr,sizeof(cliaddr))<0)
        // {
        //     ERR_EXIT("bind");
        // }
    
        //指定服务器的地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr,0,sizeof(servaddr));
        servaddr.sin_family=AF_INET;
        servaddr.sin_port=htons(5188);
        servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    
        //客户端不需要绑定和监听
        //connect 用本地套接字连接服务器的地址
        if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
            ERR_EXIT("connect");
    
        struct packet sendbuf;
        struct packet recvbuf;
        memset(&sendbuf,0,sizeof(sendbuf));
        memset(&recvbuf,0,sizeof(recvbuf));
    
        int n;  //包的长度
        //输入字符串
        while (fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin)!=NULL)
        {
            n=strlen(sendbuf.buf);
            sendbuf.len=htonl(n);
    
            //写入套接字,writen发送定长包
            writen(sock,&sendbuf,4+n);
            //读取套接字
    
            int ret = readn(sock, &recvbuf.len, 4); //先接受4个字节,头部长度
            if (ret == -1)
            {
                ERR_EXIT("read");
            }
            if (ret < 4) //对方关闭了
            {
                printf("client_close!");
                break;
            }
            n=htonl(recvbuf.len);
            ret = readn(sock, recvbuf.buf, n);
            if (ret == -1)
            {
                ERR_EXIT("read");
            }
            else if (ret < n) //对等方关闭了
            {
                printf("client_close!");
                break;
            }
    
            fputs(recvbuf.buf,stdout);
            memset(&sendbuf,0,sizeof(sendbuf));
            memset(&recvbuf,0,sizeof(recvbuf));
        }
    
        //关闭套接字
        close(sock);
    
        return 0;
    }

    发送方:先发送包体长度,再发送数据包体

    接收方:先接受长度,在接受对应长度的包体

    这样,就进行了消息和消息的边界的区分,解决了粘包问题

    21.recv函数

    MSG_PEEK可以接受缓冲器的数据,但是并不将数据从缓冲区清除

    但read函数在接收过程中将缓冲区数据清除,一次读将一整行完全读走了,实际上是不可考的,因为TCP是流的形式,消息与消息之间是无边际的,不能假定一次读就返回了整个消息,在应用层可以用 区分消息之间的边界,因为一行一行发送数据,每一行都有一个 字符。

    读取一行带 的数据的封装函数readline

    //有数据就接受,没有数据就阻塞
    ssize_t recv_peek(int sockfd, void *buf, size_t len)
    {
        while (1) 
        {
            int ret = recv(sockfd, buf, len, MSG_PEEK);
            if (ret == -1 && errno == EINTR)
                continue;
            return ret;
        }
    }
    //读取一行最大的字节数,如果在之前遇到
    就返回
    //很多消息是在结尾加
    ,当读到
    就结束(FTP)
    //readline函数只能用于套接口
    ssize_t readline(int sockfd, void *buf, size_t maxline)
    {
        int ret;
        int nread;
        char *bufp = buf;
        int nleft = maxline;    //剩余的字节数
        while (1) 
        {
            //接收到bufp的缓冲区中,recv_peek不会清除sockfd中的数据
            ret = recv_peek(sockfd, bufp, nleft);
            if (ret < 0)
                return ret;
            else if (ret == 0)
                return ret;
            //接收到的字节数
            nread = ret;
            int i;
            for (i=0; i<nread; i++)    //判断bufp中是否有换行符
            {
                if (bufp[i] == '
    ')
                {
                    //下标为i总共有i+1个字节
                    ret = readn(sockfd, bufp, i+1);
                    if (ret != i+1)    //接收失败
                        exit(EXIT_FAILURE);
                    return ret;
                }
            }
    
            if (nread > nleft)    //读到的字节数大于剩余的字节数
                exit(EXIT_FAILURE);
    
            nleft -= nread;    //剩余的字节
            //读取走nread个字节
            ret = readn(sockfd, bufp, nread);
            
            if (ret != nread)
                exit(EXIT_FAILURE);
    
            bufp += nread;    //下一次的指针偏移量
        }
    
        return -1;
    }

    e819e373-75ad-4fd3-bb20-fc153571405c

    22.改进后的回射客户/服务器程序2(使用readline):

    服务器

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <error.h>
    #include <string.h>
    #include <errno.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)
    {
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nread = read(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nread == 0)
                return count - nleft;
    
            bufp += nread;
            nleft -= nread;
        }
    
        return count;
    }
    
    ssize_t writen(int fd, const void *buf, size_t count)
    {
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nwritten = write(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nwritten == 0)
                continue;
    
            bufp += nwritten;
            nleft -= nwritten;
        }
    
        return count;
    }
    
    //有数据就接受,没有数据就阻塞
    ssize_t recv_peek(int sockfd, void *buf, size_t len)
    {
        while (1)
        {
            int ret = recv(sockfd, buf, len, MSG_PEEK);
            if (ret == -1 && errno == EINTR)
                continue;
            return ret;
        }
    }
    
    //读取一行最大的字节数,如果在之前遇到
    就返回
    //很多消息是在结尾加
    ,当读到
    就结束(FTP)
    //readline函数只能用于套接口
    ssize_t readline(int sockfd, void *buf, size_t maxline)
    {
        int ret;
        int nread;
        char *bufp = (char*)buf;
        int nleft = maxline; //剩余的字节数
        while (1)
        {
            //接收到bufp的缓冲区中,recv_peek不会清除recv_peek中的数据
            ret = recv_peek(sockfd, bufp, nleft);
            if (ret < 0)
                return ret;
            else if (ret == 0)
                return ret;
            //接收到的字节数
            nread = ret;
            int i;
            for (i = 0; i < nread; i++) //判断bufp中是否有换行符
            {
                if (bufp[i] == '
    ')
                {
                    //下标为i总共有i+1个字节
                    ret = readn(sockfd, bufp, i + 1);
                    if (ret != i + 1) //接收失败
                        exit(EXIT_FAILURE);
                    return ret;
                }
            }
    
            if (nread > nleft) //读到的字节数大于剩余的字节数
                exit(EXIT_FAILURE);
    
            nleft -= nread; //剩余的字节
                            //读取走nread个字节
            ret = readn(sockfd, bufp, nread);
    
            if (ret != nread)
                exit(EXIT_FAILURE);
    
            bufp += nread; //下一次的指针偏移量
        }
    
        return -1;
    }
    
    void do_service(int sock)
    {
        char recvbuf[1024]={0};
        while (1)
        {
            memset(recvbuf,0,sizeof(recvbuf));
            int ret=readline(sock,recvbuf,sizeof(recvbuf));
            if (ret == -1)
            {
                ERR_EXIT("read");
            }
            else if (ret == 0)
            {
                printf("peer close
    ");
                break;
            }
            writen(sock,recvbuf,strlen(recvbuf));
        }
        close(sock);
    }
    ///////////////////////////////////////////////////
    
    int main(void)
    {
        //socket
        int listenfd;
        //listenfd=socket(PF_INET,SOCK_STREAM,0);
        if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        {
            ERR_EXIT("socket");
        }
    
        //填充地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        //servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
        //inet_aton("127.0.0.1",&servaddr.sin_addr);
    
        //地址复用
        int on = 1;
        if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        {
            ERR_EXIT("setsocketopt");
        }
    
        //bind 绑定listenfd和本地地址结构
        if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        {
            ERR_EXIT("bind");
        }
    
        if (listen(listenfd, SOMAXCONN) < 0)
        {
            ERR_EXIT("listen");
        }
    
        // 调用listen函数后,就成了被动套接字,否则是主动套接字
        // 主动套接字:发送连接(connect)
        // 被动套接字:接收连接(accept)
    
        //对方的地址
        struct sockaddr_in peeraddr;
        socklen_t peerlen = sizeof(peeraddr);
        int conn; //已连接套接字(主动)
    
        pid_t pid;
        while (1)
        {
            if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
            {
                ERR_EXIT("accept");
            }
            //连接成功后打印客户端的ip和端口
            printf("client: ip=%s | port=%d
    ", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
    
            pid = fork();
            if (pid == -1)
            {
                ERR_EXIT("fork");
            }
            if (pid == 0)
            {
                /* 子进程的处理 */
                close(listenfd); //子进程不需要处理监听,子进程处理通信细节
                //通信处理封装函数
                do_service(conn);
                //一旦客户端关闭进程返回了,这个子进程就要结束
                exit(EXIT_SUCCESS);
            }
            else
            {
                /*父进程的处理 */
                close(conn); //父进程不需要处理连接
            }
        }
        return 0;
    }

    客户端

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <error.h>
    #include <string.h>
    #include <errno.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)
    {
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nread = read(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nread == 0)
                return count - nleft;
    
            bufp += nread;
            nleft -= nread;
        }
    
        return count;
    }
    
    ssize_t writen(int fd, const void *buf, size_t count)
    {
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char *)buf;
    
        while (nleft > 0)
        {
            if ((nwritten = write(fd, bufp, nleft)) < 0)
            {
                if (errno == EINTR)
                    continue;
                return -1;
            }
            else if (nwritten == 0)
                continue;
    
            bufp += nwritten;
            nleft -= nwritten;
        }
    
        return count;
    }
    
    //有数据就接受,没有数据就阻塞
    ssize_t recv_peek(int sockfd, void *buf, size_t len)
    {
        while (1)
        {
            int ret = recv(sockfd, buf, len, MSG_PEEK);
            if (ret == -1 && errno == EINTR)
                continue;
            return ret;
        }
    }
    
    //读取一行最大的字节数,如果在之前遇到
    就返回
    //很多消息是在结尾加
    ,当读到
    就结束(FTP)
    //readline函数只能用于套接口
    ssize_t readline(int sockfd, void *buf, size_t maxline)
    {
        int ret;
        int nread;
        char *bufp = (char*)buf;
        int nleft = maxline; //剩余的字节数
        while (1)
        {
            //接收到bufp的缓冲区中,recv_peek不会清除recv_peek中的数据
            ret = recv_peek(sockfd, bufp, nleft);
            if (ret < 0)
                return ret;
            else if (ret == 0)
                return ret;
            //接收到的字节数
            nread = ret;
            int i;
            for (i = 0; i < nread; i++) //判断bufp中是否有换行符
            {
                if (bufp[i] == '
    ')
                {
                    //下标为i总共有i+1个字节
                    ret = readn(sockfd, bufp, i + 1);
                    if (ret != i + 1) //接收失败
                        exit(EXIT_FAILURE);
                    return ret;
                }
            }
    
            if (nread > nleft) //读到的字节数大于剩余的字节数
                exit(EXIT_FAILURE);
    
            nleft -= nread; //剩余的字节
                            //读取走nread个字节
            ret = readn(sockfd, bufp, nread);
    
            if (ret != nread)
                exit(EXIT_FAILURE);
    
            bufp += nread; //下一次的指针偏移量
        }
    
        return -1;
    }
    
    void do_service(int sock)
    {
        char sendbuf[1024]={0};
        char recvbuf[1024]={0};
        while (fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
        {
            writen(sock,sendbuf,strlen(sendbuf));
            int ret=readline(sock,recvbuf,sizeof(recvbuf));
            if(ret==-1)
            {
                ERR_EXIT("readline");
            } 
            else if(ret==0)
            {
                printf("server close
    ");
                break;
            }
            fputs(recvbuf,stdout);
            memset(sendbuf,0,sizeof(sendbuf));
            memset(recvbuf,0,sizeof(recvbuf));
        }
        close(sock);
    }
    /////////////////////////////////////////////////////////////////
    int main()
    {
        //socket
        int sock;
        if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        {
            ERR_EXIT("socket");
        }
    
        // struct sockaddr_in cliaddr;
        // memset(&cliaddr, 0, sizeof(cliaddr));
        // cliaddr.sin_family = AF_INET;
        // cliaddr.sin_port = htons(2019);
        // cliaddr.sin_addr.s_addr = htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
        // if(bind(sock,(struct sockaddr*)&cliaddr,sizeof(cliaddr))<0)
        // {
        //     ERR_EXIT("bind");
        // }
    
        //指定服务器的地址结构
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
        //客户端不需要绑定和监听
        //connect 用本地套接字连接服务器的地址
        if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("connect");
        do_service(sock);
        close(sock);
        return 0;
    }

    23.获得已连接的本地的套接口getsockname

    原型

    #include <sys/socket.h>
    int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    参数

    sockfd:套接字

    add:接收返回结果的地址结构

    addrlen:接收地址的长度

    返回值:

    若无错误发生,getsockname()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。

    connect...
    ...
    struct sockaddr_in localaddr;
    socklen_t addrlen=sizeof(localaddr);
    gersockname(sock,(struct sockaddr*)&localaddr,&addrlen)
    
    printf("client: ip=%s | port=%d
    ",inet_ntoa(loacaladdr.sin_addr),ntohs(localaddr.sin_port));
    ...

    24.获得已连接的对等方的套接口getpeername

    原型:

    #include <sys/socket.h>
    int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    只有在已经连接后才能获得对等方的套接口信息

    参数:addr还是接收返回结果的地址结构

    返回值:成功0,失败-1。

    25.获取主机名称gethostname

    原型:

    #include <unistd.h>
    int gethostname(char *name, size_t len);

    参数:

    name:接收返回值的空间

    len:空间的大小

    返回值:成功0,失败-1。

    26.通过主机名获取主机下的所有IP地址gethostbyname

    原型:

    #include <netdb.h>
    extern int h_errno;
    struct hostent *gethostbyname(const char *name);
    char host[100]={0}
    if(gethostname(host,sizeof(host))<0)
        ERR_EXIT("gethostname");
        
    struct hostent *hp;
    if(hp=gethostbyname(host)==NULL)
        ERR_EXIT("gethostname");
    
    int i=0;
    while(hp->h_addr_list[i]!=NULL)
    {
        cout<<inet_ntoa((struct in_addr*)hp->h_addr_list[i]);    //先强转为struct in_addr结构,再将其网络地址转换为点分十进制
        i++;
    }

    hostent结构体指针

    The hostent structure is defined in <netdb.h> as follows:
    struct hostent {
        char  *h_name;            /* official name of host */
        char **h_aliases;         /* alias list */
        int    h_addrtype;        /* host address type */
        int    h_length;          /* length of address */
        char **h_addr_list;       /* list of addresses (保存地址列表)*/
    }
    #define h_addr h_addr_list[0] /* for backward compatibility (如果只获取第一个IP,可以用h_addr宏来代替)*/

    27.获得本机IP(第一个IP)函数的封装

    int getlocalip(char* ip)    //ip是用来存储返回的地址的空间
    {
        char host[100]={0};
        if(gethostname(host,sizeof(host))<0)
            return -1;
        struct hostent *hp;
        if((hp=gethostbyname(host))==NULL)
            return -1;
        strcpy(ip,inet_ntoa(*(struct in_addr*)hp->h_addr_list[0]));    //只获得第一条IP
        return 0;
    }

    如果只获取第一个IP,可以用h_addr宏来代替

    ...
    strcpy(ip,inet_ntoa(*(struct in_addr*)hp->h_addr));    //只获得第一条IP
  • 相关阅读:
    jQuery第1天
    JS基础-第5天
    JS基础-第1天
    移动开发day4_京东移动页面
    关于Python脚本通过crontab调度的时候报错UnicodeEncodeError: ‘ascii’ codec can’t encode characters in positi的解决方案
    关于redshift数据库当中的STL_LOAD_ERRORS问题的解决
    关于对key-value的数据行转化为列的sql操作
    关于mongodb当中的数据导入到mysql数据。
    关于hive当中的窗口分析函数总结
    关于hive当中表的存储和压缩方式总结
  • 原文地址:https://www.cnblogs.com/WindSun/p/11277106.html
Copyright © 2011-2022 走看看