Socket通信基础

    xiaoxiao2023-03-24  5

    Socket通信

    简介

    Pipe、FIFO、消息队列、semaphores和shared memory等经典的IPC方法。这些方法只是允许同一机器上的不同进程间进行通信。本节,我们将会介绍不同机器上运行的进程间的通信机制(与同一网络连接):网络IPC。

    本章将描述socket 网络IPC的接口,进程可以使用这些接口来和另外的进程进行通信,不管是在同一机器还是在不同的机器上。这也是socket设计的一个主要目的,适合不同机器间的进程进行通信,我们使用的是TCP/IP协议,尽管socket可以使用很多别的网络协议,但是TCP/IP协议实际上是网络传输的标准协议。

    通过Socket进行网络通讯和现实生活中打电话有着惊人的相似;

    A想要给B打电话,首先,A必须有一台电话机(相当于socket),并且要知晓对方的电话号码,然后向对方拨号,相当于发出链接请求;(假若对方不在同一区内,还需要加区号,相当于网络地址);假如对方在并且空闲的话,(相当于通信的另一方可以接受链接请求);这样双方就可以通话了,相当于建立链接;那么通话的过程就是简单的读写socket过程;通话完毕,挂掉电话,就撤销链接;

    Socket层在网络通信中的位置:

    socket描述符

    Socket是通信终端的一种抽象,应用程序使用socket描述符来访问socket。

    创建一个socket

    int socket(int domain, int types, intprotocol)

    其中的domain表示AF_说明address family(地址族),一般有INET\INET6\UNIX\UNSPEC,而AF_LOCAL是AF_UNIX的别名。

    Type表明socket的类型,从而决定通信的特征,最常用的是SOCK_DGRAM 和SOCK_STREAM两种。

    Protocol表明是在有多种选择的前提下可以指定一种protocol,通常,SOCK_DGRAM默认的是UDP,而SOCK_STREAM默认的是TCP。

    我们可以通过使用shutdown函数挂起套接字的I/O功能

    int shutdown(int sockfd, int how)

    当how为SHUT_RD时,则表明是不能读数据

    为SHUT_WR说明不能写数据(传输)

    为SHUT_RDWR表明既不能读也不能传输数据

    既然可以使用close方法,那么为什么还要用shutdown呢?

    Close方法是在socket的引用全部关闭之后才会重新分配网络终端,这就是说如果我们复制了socket,那么在所有引用这个socket的文件描述符关闭之前是不能够重新分配这个socket的,然而shutdown则可以使socket不活跃,并不依靠引用的个数或者状态。另外,shutdown只关闭socket的一端是很方便的,只允许在某一端写或者是读,可以用来测试通信的另一方的状态等。

    多路复用

    select、poll、epoll函数

    select实现同步IO多路复用

     

    poll实现轮询,等待监听的socket fd上发生某些希望得到的事件。

    例程:

    struct pollfd pfd; pfd.fd = listen_sd; pfd.events = POLLIN | POLLERR | POLLNVAL | POLLHUP; intr = poll(&pfd, 1, -1);// 循环轮询pfd中的events,只轮询一个socket

    tcp状态图

    epoll有两种模式:

    Edge Triggered(简称ET,边缘触发) 和 LevelTriggered(简称LT,水平触发)

    在采用这两种模式时要注意的是:

    如果采用ET模式,仅当状态发生变化时才会通知;

    而采用LT模式类似于原来的select/poll操作,只要还有没有处理的事件就会一直通知;

    边缘触发可能出现的问题

    寻址

    在使用socket之前,我们必需知道如何鉴定通信的另一个进程。要鉴定一个进程,从以下两方面:机器的网络地址(我们想要连接的对象)、service(服务)帮助我们定位是哪个进程。

    字节序

    同一台机器上不必担心字节的排序。字节序是处理器的架构的特征,说明字节是怎样排列的。

    分为MSB LSB两个部分:各占四个字节,就是一个数据的内存存放的顺序问题,有的是左高右低、有的右高左低。对于linux和freeBSD来说,是左高右低;对MAC OS和Solaris来说,是右高左低的方式。

    网络协议指定字节序,因此不同的计算机系统可以交换协议的信息而不需顾虑字节序。TCP/IP协议套件使用的是左低右高的字节序。对于应用程序来说,在交换格式化的数据时字节序是透明的。对TCP/IP来说,地址是以网络字节序显示出来的,因此应用程序需要将他们从处理器的字节序和网络的字节序间转换。

    较为普遍的函数是htonl(uint32_t htonl32):将处理器的字节序转换为32比特网络的字节序(为TCP/IP)【host to network long】

    地址格式

    在一个特定的通信域中,地址被视为一个socket终端,不过一般的sockaddr地址结构为:

    Struct sockaddr{ Sa_family_t sa_family; Char sa_data[14]; };

    那么网络的地址结构为:

    Struct in_addr { In_addr_t s_addr; }; Struct sockaddr_in { Sa_family_t sa_family;//32位 In_port_t sin_port;//16位 Struct in_addr sin_addr; };

    我们可以看到UNIX 域的socket地址和网络域的socket地址形式是不同的。

    BSD网络软件将二进制地址变为十进制的点地址

    查找地址

    观点上,应用程序不需要知道socket地址的内部结构。只要简单的传递socket的地址就可以了,并不需要依赖于任何协议特有的特征。

    将地址和socket联系起来

    可以让系统为我们选择一个默认的地址。对于服务器来说,我们必须将众所周知的地址和服务器的socket绑定在一起,这样客户端将会通过这个socket。客户端需要知道连接服务器socket的地址,最简单的方法是服务器将地址在/etc/services中注册。

     

    使用bind函数将socket和地联系在一起

    bind(int sockfd, conststruct sockaddr * addr, socklen_t len)

    获取一个绑定到socket的地址getsockname(int sockfd, struct sockaddr)

     

    若socket连接到一个peer,获取peer的地址

    getpeername(int sockfd, structsockaddr,……)

    若出现错误:failed to getpeername (107) Transport endpoint is not connected

    可能链接已经关闭,但是客户端仍然期望获取peername,导致报错。

    建立连接

    在面向连接的网络服务中,在交换数据之前,我们必须在要求服务的进程的socket(客户端)和提供服务的进程(服务器)之间建立一个连接。使用connect函数来完成

    Connect(int sockfd,const structsockaddr, socklen_t len)

    我们传入的地址是我们期望与之通信的服务端的地址,如果sockfd没有绑定到一个地址上,那么会为调用者选择默认的地址。

    当我们想连接到服务器,以下的情况下连接请求会失败:想要连接的机器必须是up和正在运行的,服务器需要绑定到我们试图连接的地址上。

    一个指数补偿的例子:若connect失败了,则进程会睡眠一小会在进行尝试,每次增加延迟的时间直到最大的大约2min

    <pre name="code" class="cpp">int connect_retry(int sockfd, conststruct sockaddr *addr, socklen_t alen) { int nsec; for(nsec = 1; nsec <= MAXSLEEP; nsec<<= 1){ if (connect(sockfd, addr,alen) == 0) { return(0); } if (nsec <= MAXSLEEP/2) sleep(nsec); } return(-1); }

    一旦服务器调用了listen,使用的socket就能接受连接请求。使用accept函数来去除连接请求并转换为connection,accept函数返回的是连接到客户端的套接字描述符sockfd,而accept函数的参数列表中的sockfd不和connection联系起来,而是可以接收其他的connect请求。

    数据传输

    Socket终端看成是文件描述符,那么我们可以使用read和write函数来和socket通信,只要连接建立了。可以像对本地文件一样操作socket。有专门为socket设计的6个函数,三个是接受数据,三个是传送数据。

    传送数据:send函数,发送数据到连接的另一端。有的协议还支持消息界限,就是传输的数据的长度超过规定的值的话,那么会返回错误。对于字节流的协议,send函数将会阻塞直到所有的数据都传输完毕了。

    如果是面向连接的socket,那么目的地址可以忽略,因为目的地址会隐含在connection中。如果是无连接的socket,那么我们不能使用send函数除非已经通过调研connect函数设置了目的地址,这样的话,sendto就给我们多了一种选择。另外的一种选择是,sendmsg函数,使用的是msghdr结构体。

    Recv函数类似于read函数

    recv(int sockfd, void *buf, size_t nbytes, int flags)

    socket选项

    Socket机制提供了两个socket选项接口来控制socket的行为。一个是用来设置选项,另一个允许我们询问选项的状态。三种协议:所有的socker通用的、依赖于底层通信协议的、单个协议支持的。

    setsockopt(int sockfd, int level, intoption,……)

    其中level选项说明使用的协议。若是通用的socket层的协议,就设置level为SOL_SOCKET.

    否则的话,level设置为控制option的协议的number。

    转载请注明原文地址: https://ju.6miu.com/read-1201431.html
    最新回复(0)