Socket套接字

18.3  Socket套接字

Socket 套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用 程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广 泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。

18.3.1  Socket套接字简介

Socket的英文原意是“插座”,作为类UNIX系统的进程通信机制,它如同插座一样方便的帮助计算机接入互联网通信。

任 何用户在通信之前,首先要先申请一个Socket号,Socket号相当于自己的电话号码。同时要知道对方的电话号码,相当于对方有一个Socket。然 后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一区内,还要拨对方区号,相当于给出网络地址)。对方假如在场并空闲(相当于通信的另一主机开机且 可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是向电话机发出信号和从电话机接受信号的过程,相当于 Socket发送数据和从Socket接受数据。通话结束后,一方挂起电话机,相当于关闭Socket,撤销连接。

由此可见,Socket的通信机制与电话交换机制非常相似。Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。每一个Socket都用一个半相关描述。

{协议,本地地址,本地端口}。

一个完整的Socket则用一个相关描述:

{协议,本地地址,本地端口,远程地址,远程端口}。

每 一个Socket有一个本地的唯一Socket号,由操作系统分配。套接字有3种类型:流式套接字(SOCK_STREAM)、数据包套接字 (SOCK_DGRAM)和原始套接字。流式套接字可以提供可靠的、面向连接的通信流。如果通过流式套接字发送了顺序的数据:1、2。那么数据到达远程时 候的顺序也是1、2。流式套接字可用于Telnet远程连接、WWW服务等需要使数据顺序传递的应用,它使用TCP协议保证数据传输的可靠性。流式套接字 的工作原理如图18.9所示,我们将网络中的两台主机分别作为服务器和客户机看待。

数据包套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠性。数据包套接字使用者数据包协议UDP,数据只是简单地传送到对方。数据包套接字的工作原理如图18.10所示。

原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。

18.3.2  创建套接字

套接字是通过标准的UNIX文件描述符和其他的程序通信的一个方法。套接字在使用前必须先被建立,建立套接字的系统调用为socket(),它的一般形式是:

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

创建出来的套接字是一条通信线路的一个端点,domain参数负责指定地址族,type参数负责指定与这个套接字一起使用的通信类型,而protocol参数负责制定所使用的协议。domain参数的取值范围如表18.2所示。

表18.2  domain参数的取值范围

参    数

说    明

AF_UNIX

UNIX内部(文件系统套接字)

AF_INET

ARPA因特网协议(UNIX网络套接字)

AF_ISO

ISO标准协议

AF_NS

施乐网络系统协议

AF_IPX

NOVELL IPX协议

AF_APPLETALK

Appletalk DDS

最常用的套接字域是AF_UNIX 和AF_INET,前者用于通过UNIX文件系统实现的本地套接字,后者用于UNIX网络套接字。AF_INET套接字可以用在穿过包括Internet在内的各种TCP/IP网络而进行通信的应用程序中。

套 接字参数type指定了与新套接字对应的通信特性。它的取值范围为枚举常量SOCK_STREAM和SOCK_DGRAM。SOCK_STREAM是一个 有序的、可靠的、基于连接的双向字节流。对于一个AF_INET域的套接字来说,如果在恋歌流式套接字的两端之间建立的是一个TCP连接,连接时默认值即 为该特性。SOCK_DGRAM是一个数据图服务,可以用来发送最大长度是一个固定值的消息,但消息是否会被送达或者消息的先后次序是否会在网络传输中被 重新安排并没有保证。对于AF_INET域的套接字来说,这种类型的通信是由UDP提供的。

通信所用的协议通常是由套接字的类型和套接字的域来决定,如果还有其他的协议可以选择,那么就在protocol参数里设置。protocol参数默认值为0,表示使用默认的协议。

socket系统调用返回的是一个描述符,它与文件描述符非常相似。当这个套接字和通信线路另一端的套接字连接好以后,就可以进行数据的传输和接收操作了。

18.3.3  套接字地址

每个套接字域都有独特的地址格式。对于一个AF_UNIX 套接字来说,它的地址是由一个包含在sys/un.h头文件里的sockaddr_un结构描述的。该结构的定义为:



struct sockaddr_un {

   sa_family_t sun_family;      // AF_UNIX

   char sun_path[];             // 路径

};

因 为不同类型的地址都需要传递到对套接字进程处理的系统调用里去,所以定义各种地址格式时使用的结构也都很相似,每个结构的开始都是一个定义地址类型(即套 接字域)的数据项。sun_family_t是由X/Open技术规范定义的,在Linux系统上,它被声明为一个short类型。sun_path给出 的路径长度是有限制的,Linux规定其最长不能超过108个字符。因为地址结构在长度方面是不固定的,所以许多套接字调用都要用到或输出一个用来复制特 定地址结构的长度值。

AF_INET域里的套接字地址是由一个定义在netinet/in.h头文件里的sockaddr_in结构确定的。该结构的定义为:

struct sockaddr_in {

   short int sin_family;             // AF_INET

   unsigned short int sin_port;     // 端口号

   struct in_addr sin_addr;     // Internet地址

};

其中Internet地址是netinet/in.h头文件中定义的另一个结构体,该结构体的定义为:

struct in_addr {

   unsigned long int s_addr;

};

一个AF_INET套接字完全可以由它的域、IP地址和端口号确定下来。从应用程序的角度看,各种套接字的行为就像是文件描述符,用一个独一无二的整数就可以把它们表示出来。

18.3.4  套接字的名字

要使socket()调用创建的套接字能够被其他进程使用,程序就必须给该套接字起个名字。AF_UNIX 套接字会关联到一个文件系统的路径名上去,AF_INET套接字将关联到一个IP端口号上去。为套接字命名可使用bind()系统调用,它的一般形式如下:

int bind(int socket, const struct sockaddr * address, size_t address_len);

bind() 系统调用的作用是把参数address中给出的地址赋值给与文件描述符socket相关联的未命名套接字。地址结构的长度是通过address_len参 数传递的。地址的长度和类型取决于地址族。bind()调用需要用一个与之对应的地址结构指针指向真正的地址类型。该调用成功时将返回0,否则返回–1, 并将errno变量设置为表18.3中的值。

表18.3  bind()系统调用返回的错误代码

代    码

说    明

EBADF

文件描述符无效

ENOTSOCK

该文件描述符代表的不是一个套接字

EINVAL

该文件描述符是一个已命名套接字

EADDRNOTAVAIL

地址不可用

EADDRINUSE

该地址已经绑定一个套接字

AF_UNIX 套接字对应的错误代码比上表要多出两个,分别是EACCESS,表示权限不足,不能创建文件系统中使用的名字;ENOTDIR/ENAMETOOLONG,表示路径错误或路径名太长。

18.3.5  创建套接字队列

为了能够在套接字上接受接入的连接,服务器程序必须创建一个队列来保存到达的请求。创建队列可使用系统调用listen()完成,它的一般形式为:

int listen(int socket, int backlog);

Linux 系统可能会对队列里能够容纳的排队连接的最大个数有限制。在这个最大值的范围内,listen()将把队列长度设置为backlog个连接。在套接字上排 队的接入连接个数最多不能超过这个数字,再往后的连接将被拒绝,用户的连接请求将会失败。这是listen()提供的一个机制,在服务器程序紧张地处理着 上一个客户的时候,后来的连接将被放到队列里排队等号。backlog常用的值是5。

listen()函数成功时会返回0,否则返回–1,它的错误代码包括EBADF、EINVAL和ENOTSOCK,含义同bind()系统调用的错误代码相同。

18.3.6  接受连接

服务器上的应用程序创建好命名套接字之后,就可以通过accept()系统调用来等待客户端程序建立对该套接字的连接了。accept()的一般形式是:

int accept(int socket, struct sockaddr * address, size_t * address_len);

accept() 系统调用会等到有客户程序试图连接到由socket参数指定的套接字时才返回。该客户就是套接字队列里排在第一位的连接。accept()函数将创建出一 个新的套接字来与该客户进行通信,返回的是与之对应的文件描述符。新套接字的类型与服务器监听套接字的类型相同。

套接字必须是被bind()调用命名过的,并且还要有一个由listen()调用分配的连接队列。客户的地址将被放在address指向的sockaddr结构里。如果不关心客户的地址,可以在这里使用一个空指针。

参数address_len给出了客户结构的长度。如果客户地址的长度超过了这个值,就会被截短。在调用accept()之前,必须把address_len设置为预期的地址长度。当这个调用返回时,address_len将被设置为客户的地址结构的实际长度。

如果套接字队列里没有排队等候的连接,accept将阻塞程序,直到有客户建立连接为止。这个行为可以用O_NONBLOCK标志改变,方法是对这个套接字文件描述符调用fcntl()函数。代码如下:

int flags = fcntl(socket, F_GETFL, 0);

fcntl(socket, F_SETFL, O_NONBLOCK|flags);

如 果有排队等候的客户连接,accept()函数将返回一个新的套接字文件描述符,否则它将返回–1。其错误原因除类似于bind()调用和 listen()调用中的情况之外,还有一个EWOULDBLOCK,如果前面指定了O_NONBLOCK标志,但队列里没有排队的连接,就会出现这个错 误。如果进程阻塞在accept()调用里的时候执行被中断了,就会出现EINTR错误。

18.3.7  请求连接

当客户想要连接到服务器的时候,它会尝试在一个未命名套接字和服务器的监听套接字之间建立一个连接。它们用connect()系统调用来完成这一工作,它的一般形式是:

int connect(int socket, const struct sockaddr * address, size_t address_len);

参 数socket指定的套接字将连接到参数address指定的服务器套接字上去,服务器套接字的长度由参数address_len指定。套接字必须是通过 socket调用获得的一个有效的文件描述符。如果操作成功,函数返回0,否则返回–1。该函数产生的错误代码如表18.4所示。

表18.4  connect()系统调用返回的错误代码

代    码

说    明

EBADF

文件描述符无效

EALREADY

套接字上已经有了一个正在使用的连接

ETIMEDOUT

连接超时

ECONNREFUSED

连接请求被服务器拒绝

如 果连接不能立刻建立起来,connect()会阻塞一段不确定的倒计时时间,这段倒计时时间结束后,这次连接就会失败。如果connect()调用是被一 个信号中断的,而这个信号又得到了处理,connect还是会失败,但这次连接尝试是成功的,它会以异步方式继续尝试。

类 似于accept()调用,connect()的阻塞特性可以用设置该文件描述符的O_NONBLOCK标志的办法来改变。在这种情况下,如果连接不能立 刻建立起来,connect()会失败并把errno变量设置为EINPROGRESS,而连接将以异步方式继续尝试。

异步连接的处理是比较困难的,而我们可以在套接字文件描述符上用一个select()调用来表明该套接字已经处于写就绪状态。

18.3.8  关闭连接

系 统调用close()函数可以结束服务器和客户上的套接字连接,就像对底层文件描述符进行操作一样。要想关闭套接字,就必须把服务器和客户两头都关掉才 行。对服务器来说,应该在read()返回0时进行该操作,但如果套接字是一个面向连接的类型并且设置了SOCK_LINGER选项,close()调用 会在该套接字尚有未传输数据时阻塞。

18.3.9  套接字通信

本节将设计两个例子演示套接字通信的过程,其中一个为服务器程序,另一个为客户程序。

1.服务器程序

服务器程序的代码如下:

客户端程序向本地的9734端口请求连接,如果连接成功即发送一个字符A作为消息。然后从服务器传送的消息中读取一个字符,并将该字符输出,退出程序。

将 这两个程序分别编译,然后打开两个终端,第一个终端运行服务器程序,这时会出现提示符“服务器等待消息”。第二个终端运行客户程序,客户程序会将字符A作 为消息传送给服务器程序,然后服务器程序对该字符进行加1处理,传送回客户程序。客户程序的输出是“来自服务器的消息是B”。这样两个程序就完成了连接和 通信。结束客户端程序可使用组合键Ctrl + C。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值