Linux下基于TCP的Socket编程

来源:互联网 发布:c语言数据类型范围 编辑:程序博客网 时间:2024/05/14 10:33

  一直都觉得linux是一个很高大上的操作系统,对它充满兴趣,但是真心觉得它上手好难,因为是零基础开始学linux,连安装一个ubuntu,就安装了三天,总是出问题,但是又没人可以请教,就只能自己百度,有的时候明明一个懂的人提点一下,就可以解决的问题,所以我就想要不要报一个培训班,正好我认识一个学长他就是做嵌入式的,和他聊了一下,他推荐我去汇文,他说那里真的可以学到东西,我又自己咨询了一下老师,最后决定参加杭州汇文的嵌入式培训。在这里,我们从简单的c语言开始到网络编程再到开发板,一路走来,收获了许多。老师讲解的十分到位,从c语言到高级编程,让我一步一步走入linux的世界。在学习中,难免会遇到问题,自己死命都解决不了的问题,到了老师面前就是小case,前段时间,我们学习TCP/IP协议,因为我是自动化专业的学生,没接触过网络协议,理解起来比较吃力,老师就画图给我们讲解,每次下课,老师都先坐一下,等到没有同学问问题了,才会离开。下面的内容是我对老师讲解的基于TCP的socket编程的总结,希望可以帮助到大家。

网络字节序:

    *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位短整数。例 如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

socket地址的数据类型:

    *socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面 要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同,如下图所示:

                            

    struct sockaddr {

     unsigned short sa_family;   //16位地址类型

     char sa_data[14];     //14字节地址数据

    };

     struct sockaddr_in {

     short sin_family;                     //16位地址类型

     unsigned short sin_port;         //16位端口号

     struct in_addr sin_addr;         //32位IP地址

     unsigned char sin_zero[8];       //8字节填充

    };

    *IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位 端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址 和一些控制字段。

    *UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。

    *IPv4、IPv6和UNIX Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:

          truct sockaddr_in servaddr;

           bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    *基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示 和in_addr表示之间转换。 

        字符串转in_addr的函数:

                #include <arpa/inet.h>

                int inet_aton(const char *strptr, struct in_addr *addrptr);

                in_addr_t inet_addr(const char *strptr);

                int inet_pton(int family, const char *strptr, void *addrptr);

        in_addr转字符串的函数:

                char *inet_ntoa(struct in_addr inaddr);

                const char *inet_ntop(int family, const void *addrptr, char  *strptr, size_t len); 

        其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

基于TCP协议的客户端/服务器程序的一般流程:

        

服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态;

客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后 从accept()返回。 

DDOS攻击:SYN输入不存在的地址,服务器处理后,返回给源机器,因为根本就不存在这样的地址,所以服务器无法接到应答

数据传输的过程:

建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户 端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻 调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调 用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器 的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一 方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。 

服务器端:

   #include <sys/socket.h>

    int socket(int family, int type, int protocol);                    //打开一个网络通讯端口,全双工的管道

        socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示 面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的 传输协议。protocol参数的介绍从略,指定为0即可。

    int bind(int sockfd, const struct sockaddr *myaddr,socklen_t  addrlen);    //绑定一个固定的网络地址和端口号

        服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址 和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端 口号。bind()成功返回0,失败返回-1。

        bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。我们的程序中对myaddr参数是这样初始化的: 

        bzero(&servaddr, sizeof(servaddr));        //整个结构体清零

        servaddr.sin_family = AF_INET;                //设置地址类型为AF_INET,即IPv4

        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   /* 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设 置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址*/

        servaddr.sin_port = htons(SERV_PORT);            //端口号为SERV_PORT

    int listen(int sockfd, int backlog);            //声明sockfd处于监听状态

        典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

    int accept(int sockfd, struct sockaddr *cliaddr, socklen_t  *addrlen);     //等待客户端的连接,读取客户端的地址和端口号

        三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连 接请求,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。如果给cliaddr参数传NULL,表示不关心客户端的地址。

        addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。

    我们的服务器程序结构是这样的:

        while (1) {

            cliaddr_len = sizeof(cliaddr);

            cli_fd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len); 

          if(cli_fd==-1)

            {

                    perror("accept");

                    return 1;

            }

            if(cli_fd>=0)                    //连接成功

            {

                    err=pthread_create(&id,NULL,thread_fun,(void*)cli_fd);   //创建线程

                    if(err)                  //返回值非0,创建失败

                    {

                            printf("pthread_create fail with error %d\n",err);

                            return 1;

                    }

            }

        }

        整个是一个while死循环,由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。当没有客户端请求连接时,程序阻塞在accept()函数这里,accept()的参数sockfd是先前的监听文件描述符, 而accept()的返回值是另外一个文件描述符cli_fd,之后与客户端之间就通过这个cli_fd通讯,我们的程序是当有客户端请求连接并且连接成功后,我们会创建一个线程,线程函数的最后的一个参数我们传递的是(void*)cli_fd,在线程中获取这个值,就可以在线程函数中进服务器与客户端的通信了,当断开连接时,要关闭cli_fd从而断开连接,而不关闭sockfd。

客户端:

        由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

     int socket(int family, int type, int protocol);          //打开一个网络通讯端口

            sockfd = socket(AF_INET, SOCK_STREAM, 0);       

    int connect(int sockfd, const struct sockaddr *servaddr, socklen_t  addrlen);     //连接服务器

        客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。

         程序中对servaddr参数是这样初始化的:

            bzero(&servaddr, sizeof(servaddr));  //整个结构体清零

            servaddr.sin_family = AF_INET;      //设置地址类型为AF_INET,即IPv4

            inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); //字符串转in_addr,

            servaddr.sin_port = htons(SERV_PORT);    //端口号为SERV_PORT

以上就是我对基于TCP的socket编程的总结,如果有什么不对的地方,希望大家多多包涵,很乐于得到大家的意见,共同学习,共同进步,谢谢。

0 0