第56章 SOCKET:介绍

socket是是一种IPC方法,他允许位于同一主机(计算机),或使用网络链接起来的不同主机上的应用程序之间交换数据,第一个被广泛接受的socket API实现于1983年,出现在了4.2BSD中,实际上这组API已经被移植到了所有UNIX实现以及其他大多数操作系统上了

56.1概述

在一个典型的客户端/服务器场景中,应用程序使用socket进行通信的方式如下。

  • 各个程序创建一个socket。socket是一个允许通信的“设备”,零个程序都需要用到它。
  • 服务器将自己的socket绑定到一个众所周知的地址(名称)上似的客户端能够定位到他的位置

使用socket()系统调用能够创建一个socket,他返回一个用来在后续系统调用中引用该socket的文件描述符。

fd = socket(domain,type,protol)
//在被章节介绍的所有应用程序中,protol参数总是被指定为0

通信domain

    socket存在于一个通信domain中,他确定:

  • 识别出一个socket的方法(即socket“地址”的格式);
  • 通信范围(即是位于同一主机上的应用程序还是在位于使用一个网络连接起来的不同主机的应用程序之间)。

现在操作系统至少支持下列domain.

  • UNIX(AF_UNIX)domain允许同一主机上的应用程序之间进行通信。
  • IPv4(AF_INET)domain 允许使用因特网协议第四版网络连接起来的主机上的应用程序之间进行通信
  • IPv6(AF_INET6)domain 允许使用因特网协议第6版网络连接起来的主机上进行通信。尽管IPv6被设计成了IPv4的继任者。但目前IPv4仍然时使用最广的协议。

在一些代码中读者可能会看到名称诸如PF_UNIX而不是AF_UNIX常量在这种上下文中,AF标识“地址族(address family)”,PF表示“协议族(protocol family)”.。在一开始的时候设计人员相信单个协议族可以支持多个地址族。但是在实践中,没有哪一个协议族能够支持多个已经被定义的地址族。并且所有既有实现都见PF_常量定义成AF_常量的同义词.

下表对socket domain进行了总结

 socket 类型

    每个socket实现都至少提供两种socket:流和数据报。这两种socket类型在UNIX和Internet domain中都得到了支持.下表对这两种socket类型的属性进行了总结。

 流socket(SOCK_STREAM)提供了一个可靠的双向字节流通信信道。在这段描述中的术语含义如下。

  • 可靠的:表示可以保证发送者传输的数据会完整无缺的达到接受应用程序(假设网络连接和接受者都不会崩溃)或收到一个传输失败的通知
  • 双向的。表示数据可以在两个socket之间的任意方向上传输
  • 字节流:表示与管道一样不存在消息便捷的概念

    一个流socket类似于使用一对允许在两个应用程序之家进行双向通信的管道,他们之间的差别在于(Internet domain)socket允许在网络上通信。

   流socket的正常工作需要一对互相链接的socket,因此流socket通常被称为面向连接的。术语“对等ocket”是指链接另一端的socket,"对等地址"表示该socket的地址,“对等应用程序”表示利用这个对等socket的应用程序。类似的有时候术语“本地”被用来指连接的这一端上的应用程序,socket或地址。一个流socket只能与一个socket对等socket进行链接

数据报socket(SOCK_DGRAM)允许数据以被称为数据报的消息的形式进行交换。在数据报socket中消息边界得到了保留,但数据传输是不可靠的,消息的得到可能是无序的、重复的、或者根本就无法到达。

数据报socket是更一般的无连接socket概念的一个示例。与流socket不同,一个数据报socket在使用时无需与另外一个socket链接,(也可以与另一个socket链接,但其语义与连接的流socket是不同的)

在Internet domain中,数据报socket使用了用户数据报协议(UDP),而流socket则通常使用传输控制协议(TCP).一般来讲,在称呼这两种socket时不会使用术语“Internet domain数据报 socket”和"Internet domain 流socket",而是分别使用术语"UDP socket"和“TCP socket”

socket 系统调用

关键的socket系统调用包括以下几种。

  • socket()系统调用创建一个新socket.
  • bind()系统调用将一个socket绑定到一个低智商。通常,服务器需要使用这个调用来将其socket绑定到一个众所周知的地址上使得客户端能够定位到该socket上。
  • listen()系统调用允许一个流socket接受来自其他socket的接入链接
  • accept()系统调用在一个监听流socket上接受来自一个对等应用程序的连接。并可选的返回对等socket的地址上
  • connect()系统调用建立与另一个socket之间的连接。

    socket I/O 可以使用传统的read()和write()系统调用或使用一组socket特有的系统调用(如send() recv() sendto() recvfrom())来完成,在默认情况下,这些系统调用在I/O操作无法被立即完成时被阻塞。通过使用fcntl() F_SETFL操作来启用O_NONBLOCK打开文件状态标记可以执行非阻塞I/O.

   在Linux 上可以调用ioctl(fd,FIONREAD,&cnt)来获取文件描述符fd引用的流socket中可用的字节数。对于数据报socket来讲,这个操作会返回下一个未读数据报中的字节数(如果下一个数据报的长度为0的话就返回0)或在没有未决数据报的情况下返回0.这种特性并没有在SUSv3中规定

56.2创建一个socket socket()

    socket()系统调用创建一个新socket。

#include <sys/socket.h>
int socket(int domain,int type,int protocol);
   Returns file descriptor onsucess,or -1 on error     

    domain参数指定了socket通信的domain。type参数指定了socket类型。这个参数通常在创建流socket时会被指定为SOCK_STREAM,而在创建数据报socket时会被指定为SOCK_DGRAM.

    protocol参数在本书描述的socket类型中总会被指定为0.在一些socket类型中会使用非0的protocol值,但本书并没有对这些socket类型进行描述。如在裸socket(SOCK_RAW)中会将protocol指定为IPPROTO_RAW.

socket(0在成功时返回一个引用在后续系统调用会用到的新创建的socket的文件描述符。

56.3 将socket绑定到地址:bind()

    bind系统调用将一个socket绑定到一个地址上。

#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
    Returns 0 on success,or -1 on error

    sockfd参数是在上一个socket()调用中获得的文件描述符,addr参数是一个指针,他指向了一个指定该socket绑定到的地址的结构。传入这个参数的结构的类型取决于socket domain.addrlen参数指定了地址结构的大小。 addrlen参数使用的socklen_t数据类型在SUSv3被规定为一个整数类型。一般来讲,会将一个服务器的socket绑定到一个众所周知的地址--即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址

56.4 通用socket地址结构:structsockaddr

    传入bind()的addr和addrlen参数比较复杂,有必要对其做进一步解释,因为每种socket domain 都使用了不同的地址格式。如UNIX domain socket 使用路径名,而Internet domain socket 使用了IP地址和端口号。对于各种socket domain 都需要定义一个不同的结构类型存储socket地址。然而由于诸如bind()之类的系统调用适用于所有socket domain,因此他们必须要能够接受任意类型的地址结构。为支持这种行为,socket API 定义了一个通用的地址结构 struct sockaddr.这个类型的唯一用途是将各种domain特定的地址结构转换成单个类型以供socket系统调用中的各个参数使用。sockaddr结构通常被定义如下所示结构

struct sockaddr{
    sa_family_t sa_family;  // Adress family (AF_* constant)
    char sa_data[14];      //socket address (size varies according to socket domain)
};

这个结构时所有domain 特定的地址结构模板,其中每个地址均以与sockaddr结构中sa_family字段对应的famaliy字段大头。通过family字段的值足以确定存储在这个结构的剩余部分中的地址的大小和格式了。

56.5 流socket

流socket的运作与电话系统类似。

  1. socket()系统调用将会创建一个socket,这等于安装一个电话,为使两个应用程序能够通信,每个应用程序都必须要创建一个socket.
  2. 通过一个流socket同行类似于电话呼叫。一个应用程序在进行通信之前必须要将其socket连接到另一个应用程序的socket上。两个socket的连接过程如下

         (a)一个应用程序调用bind()以将socket绑定到一个众所周知的地址上,然后调用listen()t通知内核它接受接入连接的意愿。这一步类似于已经有一个为众人所知的电话号码并确保打开了电话,这样人们就可以打进电话了

        (b)其他应用程序通过connect()建立连接,同时指定socket的地址,这类似于拨某人的电话号码。

         (c)调用listen()的应用程序使用accect()接受连接。这类似于在电话想起时拿起电话。如果在对等应用程序调用connect()之前指定了accecpt(),那么accecpt()就会阻塞。

3. 一旦建立了连接之后就可以在两个应用程序之间(类似于两路电话会话)进行双向数据传输知道其中一个使用close()关闭为止。通信是通过read()和write()系统调用或通过一些提供了额外功能的socket特定的系统调用(send() 和recv())来完成的。下图演示流socke如何t使用这些系统调用

 主动和被动socket

  流socket通常可分为主动和被动两种。

  • 在默认情况下,使用socket()创建的socket是主动的。一个主动的socket可用在connect()调用中来建立一个到被动socket的连接,这种行为被称为执行一个主动的打开。
  • 一个被动socket(也被称为监听socket)是一个通过调用listen()以被标记成允许接入连接的socket.接受一个接入连接通常被称为执行一个被动的打开。
  • 在大多数使用流socket的应用程序中,服务器会执行被动式打开,而客户端会执行主动式打开

  56.5.1 监听接入连接:listen()  

listen()系统调用将文件描述符sockfd引用的流socket标记为被动。这个socket后面会被用来接受其他(主动的)socket连接

#include <sys/socket.h>
int listen(int sockfd,int backlog)
        Returns 0 on success,or -1 on error

  无法在一个已经连接的socket(即已经成功执行connect(0的socket或由accept()调用返回的socket)上执行listen()。

  要理解backlog参数的用途首先需要注意到客户端可能会在服务器调用accept()之前调用connect().这种情况下有可能会发生的,如服务器可能正在忙于处理其他客户端,这将会长生一个未决的连接。如下图所示

 内核必须要记录所有味觉的连接请求的相关信息,这样后续的accept()就能够处理这些请求了。backlog参数允许限制这种未决连接的数量,在这个限制内的连接请求会立即成功。之外的连接请求就会阻塞直到一个未决的连接被接受(通过accept(),)并从未决连接队列删除为止。

SUSv3 允许一个实现为backlog的可能取值规定一个上限并允许实现一个静默的将backlog值向下舍入到这个限制值。SUSv3规定应该通过在<sys/socket.h>中定义SOMAXCONN常量来发布这个限制。在Linux上,这个常量的值被定义成了128,但从内核2.4.5起,Linux允许在运行时通过Linux特有的/proc/sys/core/somaxconn 文件来调整这个值

56.5.2 接受连接:accept()

accept()系统调用在文件描述符sockfd引用的监听流socked上接受一个接入连接。如果在调用accept()时不存在未决的连接,那么调用就会阻塞到直到有链接请求为止,

#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
        Returns file descriotor on success, or -1 on error

 理解accept()的关键点是它会创建一个新socket,并且正是这个新socket会与执行connect()的对等socket进行连接。accept()调用返回的函数结果是已连接的socket的文件描述符。监听socket(sockfd)会保持打开状态,并且可以被用来接受后续的连接,一个典型的服务器应用程序会创建一个监听socket,将其绑定到一个众所周知的地址上,然后通过接受该socket上的连接来处理所有客户端请求。

  传入accept()的剩余参数会返回虽短的socket的地址,addr参数指向了一个用来返回socket地址的结构。这个参数的类型取决于socket domain(与bind一样)

addrlen参数是一个值--结果参数。他指向一个整数,在调用被执行之前必须要将这个整数初始化为addr指向的缓冲区的大小,这样内核就知道有多少空间可用于返回socket地址了。当accept()返回之后,这个整数会被设置成实际被复制进缓冲区的数据的字节数。

如果不关心对等socket的地址,那么可以将addr和addrlen分别指定为NULL和0。(如果可以的话,可以在后面某个时刻使用getpeername() 系统调用来获取对端的地址)

56.5.3 连接到对等socket :connect()

connect()系统调用将文件描述符socketfd引用的主动socket连接到地址通过addr和addrlen指定的监听socket上。

#include <sys/socket.h>
    int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
                                Returns 0 on success,or -1 on error

addrhe addrlen 参数的指定方式与bind()调用中对应参数的指定方式相同。

如果connect()失败并且希望重新进行连接,那么SUSv3规定完成这个任务的可移植的方法是关闭这个socket,创建一个新socket,在该新socket上进行重新连接。

56.5.4 流socket I/O

    一对连接的流socket在两个端点之间提供了一个双向通信信道,下图给出了UNIX domain 的情形

连接流socket上I/O的语义与管道上I/O的语义类似

  •  要执行I/O需要使用read()和write()系统调用(或使用socket特有的send()和recv调用)。由于socket是双向的,因此在连接两端都可以使用这个调用
  • 一个socket可以使用close()系统调用来关闭或在应用程序终止之后关闭。之后对等的应用程序试图从链接的另一端读取数据时将会收到文件结束(当所有缓冲都被读取之后)。如果对等应用程序试图向其socket写入数据,那么他会收到一个SIGPIPE信号,并且系统调用回单会EPIPE错误。

56.5.5 连接终止 close()

    终止一个流socket连接的常见方式时调用close(),如果多个文件描述符引用了同一个socket,那么当所有描述符被关闭之后连接就会终止

假设关闭一个连接之后对等应用程序崩溃或者没有读取错误处理了之前发送给他的数据。在这种情况下就无法知道已经发生了一个错误,如果需要确保数据被成功的读取和处理,那么就必须要在应用程序中构建某种确认协议。这通常就是一个从对等应用程序传过来的显示的确认消息构成。

56.6 数据报socket

数据报socket的运作类似于邮政系统。

  1. socket()系统调用等价于创建一个邮箱。(这里假设一个系统瘀血国家的农村中的邮政服务类似,取信和送信都是从信箱中发生的。)所有需要发送和接受数据报的应用程序都需要使用socket()创建一个数据报socket。
  2. 为允许另一个应用程序发送数据包(信),一个应用程序需要使用bind(0将其socket绑定到一个众所周知的地址上。一般来讲,一个服务器会将其socket绑定到一个众所周知的地址上,而一个客户端会通过向该地址发送一个数据报来发起通信。(在一些通信中--特别是UNIX domain--客户端如果先要接受服务器发送来的数据报的话可能还需要使用bind()将一个地址赋给其socket.)
  3. 要发送一个数据报,一个应用程序需要调用snedto(),他接收的其中一个参数是数据包发送的socket地址,这类似于将收信人的地址写道新风尚并投递这封信。
  4. 为接收一个数据报,一个应用程序需要调用recvfrom(),它在没有数据报到达时会阻塞。由于数recvfrom()允许获取发送者的地址,因此可以在需要的时候发送一个响应。(这在发送这的socket没有绑定到一个众所周知的地址上时是有用的,客户端通常会发生这种情况。)这里对这个比喻做了一点延伸,因为已投递的新建上时无需标记上发送者的地址的。
  5. 当不在需要socketd时,应用程序需要关闭socket.

    下图演示了数据报socket相关系统调用

 56.6.1 交换数据报:recvfrom和sendto()

  recvfrom()和sendto系统调用在一个数据报socket上接受和发送数据报

#include <sys/socket.h>

ssize_t recvfrom(int sockfd,void *buffer,size_t length,int flags,struct sockaddr *src_addr,socklen_t *addrlen);
                    Returns number of bytes received,0 on EOF,or -1 on error
ssize_t sendto(int sockfd,const void *buffer,size_t length,int flags,const struct sockaddr *dest_addr,socklen_t addrlen);
                            Returns number of bytes sent,or -1 on error

 这两个系统调用的返回值和前三个参数与read()和write()中的返回值和相应参数是一样的。

 第四个参数flags是一个位掩码,它控制着了socket特定的I/O特性,如果无需使用其中任何一种特性,那么可以将flags指定为0。

  src_addr和addrlen参数被用来获取指定与之通信的对等socket的地址。

  对于recvfrom()来讲,src_addr和addrlen参数会返回用来发送数据报的远程的socket的地址。(这些参数类似于accept()中的addr和addrlen参数,他们返回已连接的对等socket的地址。)

src_addr参数是一个指针,它指向了一个与通信domain匹配的地址结构。与accept()一样,addrlen是一个值-结果参数。在调用之前应该将addrlen初始化为src_addr指向的结构的大小;在返回之后,它包含了实际写入这个结构的字节数。

如果不关心发送者的地址,那么可以将src_addr和addrlen都指定为NULL.在这种情况下recvfrom()等价recv()来接收一个数据报。也可以使用read()来读取一条数据报,者等价于在使用recv()时将flags参数指定为0.

  不管length参数值是什么,recvfrom只会从一个数据报socket中读取一条消息,如果消息的大小超过了length字节,那么消息会被静默地截断为length字节。

对于sendto()来讲,dest_addr和addrlen参数指定了数据报发送地socket,这些参数地使用方式与connect()中相应参数地使用方式是一样地。dest参数是一个与通信domain匹配地地址结构,它会被初始化成目标socket地地址,addrlen参数指定了addr的大小。

56.6.2 在数据报socket上使用connect()

  尽管数据报socket是无连接的,但在数据报上应用connect()系统调用仍然是起作用的。在数据报socket上调用connect()会导致内核记录这个socket的对等的socket的地址。术语已连接的数据报socket就是指此种socket.术语非连的数据报socket是指那些没有调用connect()的数据报socket(即新数据报的默认行为)。

当一个数据报已连接之后:

  • 数据报的发送可在soket上使用write()(或者send())来完成并且会自动被发送到同样的对等socket上。与sendto一样每个write调用会发送一个独立的数据报;
  • 在这个socket上智能读取由对等socket发送的数据报。

  注意connect()的作用对数据报socket是不对称的。上面的论断只适用于调用了connect()数据报socket,并不适用于他链接的远程socket(除非对等应用程序在其socket上也调用了connect()).

通过再发起一个connect()调用可以修改一个已连接数据包socket的对等socket.此外通过指定一个地址族(如UNIX domain中的sun_family字段)为AF_UNSPEC的地址结构还可以接触对等关联关系。但需要注意的是,其他很多UNIX实现并不支持AF_UNSPEC用于这种用途

为一个数据报socket设置一个对等socket,这种做法的好处就是在该socket上传输数据时可以使用更简单的I/O系统调用,即无需使用指定了dest_addr和addrlen参数的sendto(),而只需要使用write()即可。设置一个对等socket主要对那些需要向单个对等socket(通常是某种数据报客户端)发送多个数据报的应用程序是比较有用的

56.7 总结

  socket允许在同一主机或者一个网络连接起来的不同主机上的应用程序之间通信。

  一个socket存在与一个通信domain中,通信domain确定了通信范围和用来标识socket的地址格式。SUSv3规定了 UNIX IPv4 IPv6通信domain

  大多数应用程序使用流socket和数据报socket()中的一种。流socket(SOCK_STREAM)为两个端之间提供了一颗可靠的,双向的字节流通信信道。数据报socket(SOCK_DGRAM),提供了不可靠的,无连接的,面向消息的通信。

  一个典型的socket服务器会使用socket()创建其socket,然后使用bind()将这个socket绑定到一个众所周知的地址上。服务器接着调用listen()以允许在该socket上接受连接。监听socket上的客户端连接是通过accept()来接受的,它将返回一个与客户端的socket进行连接的新socket的文件描述符。一个典型的流socket客户端会使用socket()创建一个socket,然后通过调用connect()建立一个连接指定服务器的中所周知的地址。当两个流socket连接之后就可以使用read()和write()在任何一个方向上传输数据了。一旦引用一个流socket端点的文件描述符的所有进程都执行了一个隐式或显示的close()之后,连接就会终止.

一个典型的数据报socket()服务器会使用socket()创建一个socket,然后使用bind()将其绑定在一个众所周知的地址上,由于数据报是无连接的,因此服务器的socket可以用来接受任意客户端的数据报。使用read()或socket特定的recvfrom()系统调用能够接受数据报,其中recvfrom()能够返回发送socket的地址,一个数据报客户端会使用socket()创建一个socket(),然后使用sendto()将一个数据报发送到指定的(即服务器)地址上。connect()系统调用可以用来为数据报socket设定一个对等地址。在设定玩对等地址之后就无须为发出去的数据报指定目标地址了:write()电泳可以用来发送一条数据报。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值