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

    TCP/IP

      要想理解socket首先得熟悉一下TCP/IP协议族, TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何再它们之间传输的标准。

      从字面意思来看TCP/IP是TCP和IP协议的合称,但实际上TCP/IP协议是指因特网整个TCP/IP协议族。不同于ISO模型的七个分层,TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中。

      

      应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等

      传输层:TCP,UDP

      网络层:IP,ICMP,OSPF,EIGRP,IGMP

      数据链路层:SLIP,CSLIP,PPP,MTU

      每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的。

    图1.TCP/IP系列协议的四个抽象层

     

    图2.TCP/IP的ISO模型的七个分层

     

    Socket

      网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址可以唯一标识网络中的主机,而传输层的“协议+端口可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

      那什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用,以实现进程网络中通信。  

        

    图3.socket在TCP/IP抽象层中的位置

     

      

       socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种“打开—读/写/—关闭”模式的实现,服务器和客户端各自维护一个“文件”,在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

    Socket通信流程

      socket是"打开—读/写—关闭"模式的实现,以使用TCP协议通讯的socket为例,其交互流程大概是这样子的。

     

    图4.TCP协议的socket通信流程图

     

      

      

      服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket

      服务器为socket绑定ip地址和端口号

      服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开

      客户端创建socket

      客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket

      服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端连接请求

      客户端连接成功,向服务器发送连接状态信息

      服务器accept方法返回,连接成功

      客户端向socket写入信息

      服务器读取信息

      客户端关闭

      服务器端关闭

    Socket的基本操作

      下面以TCP为例,介绍几个基本的socket接口函数。

    socket()函数

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

      socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

      正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

    •   domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INETAF_INET6AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
    •   type:指定socket类型。常用的socket类型有:SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等。
    •       protocol:顾名思义,就是指定协议。常用的协议有,IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

      注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

      

      当我们调用socket()创建一个socket时,返回的socket描述字存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则当调用connect()listen()时系统会随机分配一个端口。

    bind()函数

      正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INETAF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

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

      函数的三个参数分别为:

    •   sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
    •   addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,
        如ipv4对应的是:

      struct sockaddr_in {
          sa_family_t    sin_family; /* address family: AF_INET */
          in_port_t      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 network byte order */
      };

        ipv6对应的是:

      struct sockaddr_in6 { 
          sa_family_t     sin6_family;   /* AF_INET6 */ 
          in_port_t       sin6_port;     /* port number */ 
          uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
          struct in6_addr sin6_addr;     /* IPv6 address */ 
          uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
      };
      
      struct in6_addr { 
          unsigned char   s6_addr[16];   /* IPv6 address */ 
      };

        Unix域对应的是:

      #define UNIX_PATH_MAX    108
      
      struct sockaddr_un { 
          sa_family_t sun_family;               /* AF_UNIX */ 
          char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
      };
    • addrlen:对应的是地址的长度。

      通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。


    网络字节序与主机字节序

      主机字节序:就是我们平常说的大端和小端模式:不同CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

      a)Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

      b)Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

      网络字节序:是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节序采用Big-Endian排序方式。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

     

      所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。


      

    listen()、connect()函数

      如果作为一个服务器,在调用socket()bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。 

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

      

      listen()函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen()函数将socket变为被动类型的,等待客户的连接请求。

      connect()函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect()函数来建立与TCP服务器的连接。

    accept()函数

      TCP服务器端依次调用socket()bind()listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

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

      accept()函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

    注意accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭

    read()、write()等函数

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

    1 read()/write()
    2 recv()/send()
    3 readv()/writev()
    4 recvmsg()/sendmsg()
    5 recvfrom()/sendto()

      他们的声明如下:

    /*-------------------------------------------------------------------------------*/ 
          #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);
    /*-------------------------------------------------------------------------------*/

      read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

      write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。

      1)write的返回值大于0,表示写了部分或者是全部的数据。

      2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

      其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。 

    close()函数

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

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

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

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

       

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

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

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

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

    图5.socket中发送的TCP三次握手

     

      

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

      


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


    socket中TCP的四次握手释放连接详解

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

    图6.socket中发送的TCP四次握手

     

      图示过程如下:

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

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

    一个例子

    服务器段代码:

     1 /*
     2 2016-9-28 12:09:45
     3 @author:CodingMengmeng
     4 language:C;
     5 */
     6 
     7 //Server.
     8 #include <stdio.h>                   //用于printf等函数的调用
     9 #include <winsock2.h>                //Socket的函数调用 
    10 #pragma comment (lib, "ws2_32")      //C语言引用其他类库时,除了.h文件外,还要加入对应的lib文件(这个不同于C#)
    11 
    12 int main()
    13 {
    14     /*
    15         为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,
    16         因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。
    17         该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;
    18         操作系统利用第二个参数返回请求的Socket的版本信息。
    19         当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。
    20         以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。
    21 ----------------------------------------------------------------------------------------------------------------------------------------
    22         int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );
    23         (1)wVersionRequested:一个WORD(双字节)型数值,在最高版本的Windows Sockets支持调用者使用,高阶字节指定小版本(修订本)号,低位字节指定主版本号。
    24         (2)lpWSAData 指向WSADATA数据结构的指针,用来接收Windows Sockets 实现的细节
    25 ----------------------------------------------------------------------------------------------------------------------------------------
    26         本函数必须是应用程序或DLL调用的第一个Windows Sockets函数。
    27     */
    28     WSADATA wsaData;//用来接收Windows Sockets实现的细节
    29     WSAStartup(MAKEWORD(2, 2), &wsaData);//完成对Winsock服务的初始化
    30     SOCKET s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);//地址类型:AF_INET;socket类型:SOCK_STREAM;协议类型:TCP
    31     sockaddr_in sockaddr;//要绑定给sockfd的协议地址
    32     sockaddr.sin_family = PF_INET;//AF_INET决定了地址类型要使用ipv4地址(32位)与端口号(16位)的组合
    33     sockaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");   //需要绑定到本地的哪个IP地址|| 127.0.0.1是回送地址,指本地机
    34     sockaddr.sin_port = htons(9000);                          //需要监听的端口
    35     bind(s, (SOCKADDR*)&sockaddr, sizeof(SOCKADDR));        //进行绑定动作
    36     listen(s, 1);                                           //启动监听
    37     printf("listening on port [%d].
    ", 9000);
    38     int n;
    39 
    40         SOCKADDR clientAddr;
    41         int size = sizeof(SOCKADDR);
    42         SOCKET clientsocket;
    43         clientsocket = accept(s, &clientAddr, &size);               //阻塞,直到有新tcp客户端连接
    44         printf("***SYS***    New client touched.
    ");
    45         char* msg = "Hello, my client.
    ";
    46         char* revFlag = "Copy that!
    ";
    47         send(clientsocket, msg, strlen(msg) + sizeof(char), NULL);  //这里的第三个参数要注意,是加了一个char长度的
    48         while (TRUE)
    49         {
    50             char buffer[MAXBYTE] = { 0 };
    51             n=recv(clientsocket, buffer, MAXBYTE, NULL);//一直接收客户端socket的send操作
    52             //recv只有接收到数据才会往下执行,否则一直等待
    53             send(clientsocket, revFlag, strlen(revFlag) + sizeof(char), NULL);
    54             buffer[n] = '';
    55             printf("***Receive From Client***:    %s
    ", buffer);
    56         }
    57 
    58     closesocket(clientsocket);                                //关闭客户端socket
    59     closesocket(s);//关闭监听socket
    60 
    61     WSACleanup();                                                //卸载
    62     getchar();
    63     exit(0);
    64 }

    客户端代码:

     1 /*
     2 2016-9-28 12:11:41
     3 @author:CodingMengmeng
     4 language:C;
     5 */
     6 
     7 //Client.
     8 #include <stdio.h>                      //用于输入、输出函数的调用,printf, gets
     9 #include <winsock2.h>                   //socket头文件
    10 #include <Windows.h>                    //为了方便调试,所以加入了等待2秒才进行连接server,这里用到了sleep函数
    11 #pragma comment (lib, "ws2_32")         //socket库文件
    12 
    13 int main()
    14 {
    15     Sleep(2000);                        //沉睡2秒再连接server
    16     WSADATA wsaData;
    17     WSAStartup(MAKEWORD(2, 2), &wsaData);
    18     SOCKET s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    19     sockaddr_in sockaddr;//描述服务器socket地址
    20     sockaddr.sin_family = PF_INET;
    21     sockaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    22     sockaddr.sin_port = htons(9000);
    23     connect(s, (SOCKADDR*)&sockaddr, sizeof(SOCKADDR));//s:客户端的socket描述字;sockaddr:服务器的socket地址;sizeof(SOCKADDR):服务器socket地址的长度
    24     char buffer[MAXBYTE] = { 0 };
    25     recv(s, buffer, MAXBYTE, NULL);
    26     printf("***SERVER*** SEND:%s", buffer);
    27     while (TRUE)
    28     {
    29         memset(buffer, 0, sizeof(buffer));
    30         char* mymsg = new char[100000];
    31         printf("Say something to Server:
    ");
    32         gets(mymsg);//获取屏幕输入
    33         send(s, mymsg, strlen(mymsg) + sizeof(char), NULL);//发送给服务器
    34         recv(s, buffer, MAXBYTE, NULL);//接收服务器回传的响应,若未响应,则一直等待。
    35         printf("***Receive From Server***:    %s",buffer);
    36 
    37             
    38         /*
    39         recv函数中的bufferlength参数是可以固定值的
    40         send函数中的bufferlength参数不能固定值,需要看实际长度,并且考虑到''字符串
    41         */
    42     }
    43     closesocket(s);        //关闭客户端socket
    44     WSACleanup();        //卸载
    45     getchar();
    46     exit(0);
    47 }

      当然上面的代码很简单,也有很多缺点,这就只是简单的演示socket的基本函数使用。其实不管有多复杂的网络程序,都使用的这些基本函数。上面的服务器使用的是迭代模式的,即只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力是很弱的,现实中的服务器都需要有并发处理能力!为了需要并发处理,服务器需要创建一个新的进程或者线程去处理请求等。

    参考

    1、吴秦:Linux Socket编程(不限Linux)

    2、Samaritans:简单理解Socket

  • 相关阅读:
    219. Contains Duplicate II
    189. Rotate Array
    169. Majority Element
    122. Best Time to Buy and Sell Stock II
    121. Best Time to Buy and Sell Stock
    119. Pascal's Triangle II
    118. Pascal's Triangle
    88. Merge Sorted Array
    53. Maximum Subarray
    CodeForces 359D Pair of Numbers (暴力)
  • 原文地址:https://www.cnblogs.com/codingmengmeng/p/5916598.html
Copyright © 2011-2022 走看看