嵌入式linux网络编程之——5年程序员给你深度讲解socket套接字

<code>本文主要给大家分享网络七层概念之网络编程socket,前边的章节已经给大家讲述了链路层、物理层、网络层、应用层、传输层等,欢迎学习嵌入式网络编程的朋友关注、转载和发表评论! 

(绝对的好文,建议先收藏和转载!)/<code>

本文主要的讲述内容为:

1 socket概述

2.1Socket 的历史

2.2Socket 的功能

2.3套接字的三种类型

3套接字地址

3.1什么是 Socket?

3.2Socket 描述符

3.3一个套接字是怎样在网络上传输数据的?

4套接字的一些基本知识

4.1基本结构

4.2基本转换函数

5基本套接字调用

5.1socket()函数

5.2bind()函数

5.3connect()函数

5.4listen()函数

5.5accept()函数

5.6send()和 recv()函数

5.7sendto()和 recvfrom()函数

5.8close()和 shutdown()函数

5.9setsockopt()和 getsockopt()函数

5.10getpeername() 函数

5.11gethostname()函数


网络程序设计全靠套接字socket接收和发送信息,尽管套接字这个词好象显得有些神秘,但其实这个概念 极易理解。

这章主要讲述 Sockets API(Application Program Interface) ,以及一些其他的细节(比如Socket 的历史、数据中的常用结构等), 通过这些介绍, 使读者慢慢掌握 Linux 下的 Socket 编程。


在开始介绍有关编程的知识之前,首先让我们来了解一些与 socket 有关的背景知识


8.2.1 Socket 的历史

在 80 年代早期,远景研究规划局(Advanced Research Projects Agency, ARPA)资助了加利福尼亚大学伯克利分校的一个研究组,让他们将 TCP/IP 软件移植到 UNIX 操作系统中,并将结果提供给其他网点。作为项目的一部分,设计者们创建了一个接口,应用进程使用这个接口可以方便的进行通信。 他们决定,只要有可能就使用已有的系统调用,对那些不能方便的融入已有的函数集的情况,就再增加新的系统调用以支持 TCP/IP 功能。

这样做的结果就出现了插口接口(Berkeley Socket),这个系统被称为 Berkeley UNIX 或 BSD UNIX。由于许多计算机厂商,都采用了 Berkeley UNIX,所以许多机器上都可以使用 Socket 了。

这样,Socket 接口就被广泛使用,到现在已经成为事实上的标准。


嵌入式linux网络编程之——5年程序员给你深度讲解socket套接字

图 8-1 SOCKET 接口示意图

8.2.2 Socket 的功能

Socket 的英文原意就是"孔"或"插座" ,现在,作为 BSD UNIX 的进程通讯机制,取后一种意义。日常生活中常见的插座,有的是信号插座,有的是电源插座,有的可以接受信(或能量),有的可以发送信号(或能量)。假如电话线与电话机之间安放一个插座(相当于二者之间的接口,这一部分装置物理上是存在的)则 Socket 非常相似于电话插座。

将电话系统与面向连接的 Socket 机制相比,有着惊人相似的地方。以一个国家级的电话网为例。电话的通话双方相当于相互通信的两个进程;通话双方所在的地区(享有一个全局唯一的区号)相当于一个网络,区号是它的网络地址;区内的一个单位的交换机相当于一台主机,主机分配给每个用户的局内号码相当于 Socket 号(下面将谈到)。

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

在电话系统中,一般用户只能感受到本地电话机和对方电话号码的存在,建立通话的过程、话音传输的过程以及整个电话系统的技术细节对它都是透明的,这也与 Socket 机制非常相似。Socket 利用网间网通信设施实现进程通信,但它对通信设施的细节毫不关心,只要通信设施能提供足够的通信能力, 它就满足了。

至此,我们对 Socket 进行了直观的描述。抽象出来,Socket 实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之 前,双方必须各自拥有一台电话机一样。

套接字是一个通信端的标识符,由 IP 地址和端口号确定,如<200.136.112.75:2520>。套接字与TCP/IP 应用程序接口(API)相关联,每个 API 功能都需要一个套接字作为标识符。


两个设备之间的数据交换可以描述为报文从某一个设备上的套接字发送到另一个设备上的套接字。 两个套接字建立一个关联,该关联的组成元素如下:

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


每一个 Socket 有一个本地的唯一 Socket 号,由操作系统分配

可以建立多个 Socket,以允许几个应用共享同一个设备 IP 地址。另外,在一个设备上可以存在多个关联,这意味着一个设备可以同时与其他设备有多个连接。

最重要的是,Socket 是面向客户—服务器模型而设计的,针对客户和服务器程序提供不同的

Socket 系统调用。客户随机申请一个 Socket 号(相当于一个想打电话的人可以在任何一台入网的电话上拨叫呼叫);服务器拥有全局公认的 Socket,任何客户都可以向它发出连接请求和信息请求(相当于一个被呼叫的电话拥有一个呼叫方知道的电话号码)。

Socket 利用客户— 服务器模式巧妙的解决了进程之间建立通信连接的问题。服务器 Socket 为全局所公认非常重要。两个完全随机的用户进程之间,因为没有任何一方的 Socket 是固定的,就像打电话却不知道别人的电话号码,要通话是不可能的。


8.2.3 套接字的三种类型

套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字


1.流式套接字(SOCK_STREAM)

流式的套接字可以提供可靠的、面向连接的通讯流。如果你通过流式套接字发送了顺序的数据:

"1"、"2" 。那么数据到达远程时候的顺序也是"1"、"2"。


流式套接字可以做什么呢?你听说过 Telnet 应用程序吗?听过?哦,最常用的 BBS 服务,以及系统的远程登陆都是通过 Telnet 协议连接的。Telnet 就是一个流式连接。你是否希望你在 Telnet 应用程序上输入的字符(或汉字)在到达远程应用程序的时候是以你输入的顺序到达的?答案应该是肯定 的吧。还有 WWW 浏览器,它使用的 HTTP 协议也是通过流式套接字来获取网页的。事实上,如果你

Telnet 到一个 Web Site 的 80 端口上,然后输入 "GET 网页路径名"然后按两下回车(或者是两下

Ctrl+回车)然后你就得到了"网页路径名"所代表的网页!


流式套接字是怎样保证这种应用层次上的数据传输质量呢?它使用了 TCP(The Transmission Control Protocol)协议(可以参考 RFC-793 来得到 TCP 的细节)。TCP 保证了你的数据传输是正确的,并且是顺序的。TCP 是经常出现的 TCP/IP 中的前半部分。IP 代表 Internet Protocol(因特网协议,参考 RFC-791),IP 只处理网络路由。

面向连接服务器处理的请求往往比较复杂,不是一来一去的请求应答所能解决的,而且往往是并发 服务器。使用面向连接的套接字编程,可以通过图 2-2 来表示。

套接字工作过程如下:服务器首先启动,通过调用 socket() 建立一个套接字,然后调用 bind() 将该套接字和本地网络地址联系在一起,再调用 listen() 使套接字做好侦听的准备,并规定它的请求队列的长度,之后就调用 accept() 来接收连接。客户在建立套接字后就可调用 connect() 和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用 send() 和 recv() 来发送和接收数据。最后,待数据传送结束后,双方调用 close() 关闭套接字。


嵌入式linux网络编程之——5年程序员给你深度讲解socket套接字

图 8-2 面向连接的 socket 的工作流程


2.数据报套接字(SOCK_DGRAM)

数据报套接字(Datagram Sockets)定义了一种无连接的服务,数据通过相互独立的报文进行传输, 是无序的,并且不保证可靠,无差错。


为什么它叫做"无连接"?应该怎样处理它们呢?为什么它们是不可靠的?好的,这里有一些事实:

· 如果你发送了一个数据报,它可能不会到达。

· 它可能会以不同的顺序到达。

· 如果它到达了,它包含的数据中可能存在错误


数据报套接字也使用 IP,但是它不使用 TCP,它使用用户数据报协议 UDP(User Datagram

Protocol 可以参考 RFC 768)。


为什么说它们是"无连接"的呢?因为它(UDP)不像流式套接字那样维护一个打开的连接,你只需要把数据打成一个包,把远程的 IP 贴上去,然后把这个包发送出去。这个过程是不需要建立连接的。

UDP 的应用例子有:tftp, bootp 等。


那么,数据包既然会丢失,怎样能保证程序能够正常工作呢?事实上,每个使用 UDP 的程序都要有自己的对数据进行确认的协议。比如, TFTP 协议定义了对于每一个发送出去的数据包,远程在接受到之后都要回送一个数据包告诉本地程序: "我已经拿到了!" (一个 "ACK" 包)。如果数据包的发送者在 5 秒内没有的得到回应,它就会重新发送这个数据包直到数据包接受者回送了 "ACK" 信号。这些知识对编写一个使用 UDP 协议的程序员来说是非常必要的。


无连接服务器一般都是面向事务处理的,一个请求一个应答就完成了客户程序与服务程序之间的相 互作用。若使用无连接的套接字编程,程序的流程可以用图 2-3 表示。

嵌入式linux网络编程之——5年程序员给你深度讲解socket套接字

图 8-3无连接的 socket 工作流程

3.原始套接字

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

流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)涵盖了一般应用层次的 TCP/IP 应用。应用层位于 TCP/UDP 层之上,所以这两类套接字几乎涵盖了所有的应用层需求,也就是说几乎所有的应用程序都可以使用这两类来实现。

通常情况下程序设计人员接触的网络应用开发也涉及不到原始套接字。但是,如果深入地考虑一些 问题时,就会不知道该如何入手了,例如:

· 发送一个自定义的 IP 包。

· 发送一个 ICMP 数据报。

· 网卡的侦听模式,监听网络上的数据包。

· 自定义协议的实现。

要解决这些问题,需要了解另一类套接字,这就是原始套接字。原始套接字主要应用在底层网络编 程上,同时也是网络黑客的必备手段。例如 sniffer、拒绝服务(Dos)、IP 地址欺骗等都需要在原始套接字的基础上实现。

与原始套接字对应,之前的 TCP/UDP 的套接字称为标准套接字。标准套接字与网络协议栈的 TCP、

UDP 打交道,而原始套接字则与 IP 层级网络协议栈核心打交道。原始套接字提供以下 3 种标准套接字不具备的功能:

· 使用原始套接字可以读/写 ICMP、IGMP 分组。例如 ping 程序,就使用了原始套接字发送 ICMP

回显请求,并接受 ICMP 回显应答。原始套接字允许使用 ICMP 或 IGMP 构造的应用程序完全作为用户进程处理,而不必再增加过多的内核编码。

· 使用原始套接字可以读写特殊的 IP 数据报,内核不处理这些数据报的协议字段。大多数内核只处理 1(ICMP)、2(IGMP)、3(TCP)和 17(UDP)的数据报。但协议字段还可能为其他值。例如,OSPF 路由协议就不适用 TCP 或者 UDP,而直接使用 IP,将 IP 数据报的协议字段设为 89。因此,由于这些数据报包含内核完全不知道的协议字段,实现 OSPF 协议的 gated 程序必须使用原始套接字来读写它们。

· 使用原始套接字,利用函数 setsockopt()设置套接字选项

,使用 IP_HDRINGCL 可以对 IP 头部进行操作,因此可以修改 IP 数据和 IP 层之上的各层数据,构造自己的特定类型的 TCP 或者 UDP 的分组。


8.3 套接字地址

好了,关于 socket 的背景知识我们已经讲得够多了,下面,就让我们正式开始揭开 socket 的神秘面纱吧!


8.3.1 什么是 Socket?

大家经常谈论 "Socket"(套接字),那么一个套接字究竟是什么呢?

一个套接字可以这样来解释:它是通过标准的 UNIX 文件描述符和其他的程序通讯的一个方法。


8.3.2 Socket 描述符

使用 UNIX 的黑客高手有这么一句话:"恩,在 UNIX 系统中,任何东西都是一个文件。 "这句话描述了这样一个事实:在 UNIX 系统中,任何对 I/O 的操作,都是通过读或写一个文件描述符来实现的。

一个文件描述符只是一个简单的整形数值,代表一个被打开的文件(这里的文件是广义的文件,并不只代表不同的磁盘文件,它可以代表一个网络上的连接,一个先进先出队列,一个终端显示屏幕,以及其他的一切)。在 UNIX 系统中任何东西都是一个文件!所以如果你想通过 Internet 和另外一个程序通讯的话,你将会是通过一个文件来描述符实现的。

好的,你已经相信 Socket 是一个文件描述符了,那么我们应该怎样才能得到这个代表网络连接的文件描述符呢?你现在一定非常在意这个问题。是这样的:你首先调用系统函数 socket(),它返回一个套接字(Socket)描述符,然后你就可以通过对这个套接字描述符来进行一些操作:系统函数 send() 和 recv()(你可以使用 "man"命令来查找系统帮助:man send, man recv)。

你会想:"套接字描述符是一个文件描述符,为什么不能用对文件操作的 write() 和 read() 来进行套接字通讯呢?"事实上,write() 和 read() 是可以对套接字描述符进行操作的,但是,通过使用 send() 和 recv() 函数,你可以对网络数据的传输进行更好的控制!


8.3.3 一个套接字是怎样在网络上传输数据的?

我们已经谈过了网络协议层,那么我们还应该继续多了解一些东西:物理网络上的数据是怎样传送

的。


我们可以认为是这样的:

数据被分成一个一个的包(Packet),包的数据头(或数据尾)被第一层协议 (比如 TFTP 协议)加上第一层协议数据;然后整个包(包括内部加入的 TFTP 信息头)被下层协议再次包装(比如 UDP),在这之后数据包会再次被下层协议包装(比如 IP 协议),最后是被最底层的硬件层(物理层)包装上最后一层信息(Ethernet 信息头)。

当接收端的计算机接收到这个包后,硬件首先剥去数据包中的 Ethernet 信息头,然后内核在剥去

IP 和 UDP 信息头,最后把数据包提交给 TFTP 应用程序,由 TFTP 剥去 TFTP 信息头,最后得到了原始数据。

对流式套接字你所需要做的只是调用 send() 函数来发送数据。而对于数据报套接字,你需要自己加个信息头,然后调用 sendto() 函数把数据发送出去。Linux 系统内核中已经建立了 Transport

Layer 和 Internet Layer。硬件负责 NetworkAccess Layer。简单而有效,不是吗?


8.4 套接字的一些基本知识

好的,从现在开始,我们应该谈些和程序有关的事情了。


8.4.1 基本结构

首先,介绍一些使用套接字编程中常见的网络数据结构,这些数据结构对大家会很有帮助。

1.struct sockaddr

这个结构用来存储套接字地址。数据定义:

struct sockaddr

{

unsigned short sa_family; /* address 族, AF_xxx */ char sa_data[14]; /* 14 bytes 的协议地址 */

};

sa_family 一般来说,都是 "AFINET"。


sa_data 包含了一些远程电脑的地址、端口和套接字的数目,它里面的数据是杂溶在一切的。


为了处理 struct sockaddr,程序员建立了另外一个相似的结构 struct sockaddr_in:struct sockaddr_in ("in" 代表 "Internet")

struct sockaddr_in

{

short int sin_family; /* Internet 地址族 */ unsigned short int sin_port; /* 端口号 */ struct in_addr sin_addr; /* Internet 地址 */

unsigned char sin_zero[8]; /*添 0(和 struct sockaddr 一样大小)*/

};


这个结构提供了方便的手段来访问 socket address(struct sockaddr)结构中的每一个元素。注


意 sin_zero[8] 是为了是两个结构在内存中具有相同的尺寸, 使用 sockaddr_in 的时候要把

sin_zero 全部设成零值(使用 bzero()或 memset()函数)。而且,有一点很重要,就是一个指向 struct

sockaddr_in 的指针可以声明指向一个 sturct sockaddr 的结构。所以虽然 socket() 函数需要一个structaddr * ,你也可以给他一个 sockaddr_in * 。注意在 struct sockaddr_in 中,sin_family 相当于在 struct sockaddr 中的 sa_family, 需要设成"AF_INET"。

最后一定要保证 sin_port 和 sin_addr 必须是网络字节顺序(见下节)!


2.struct in_addr

其定义如下:

/* 因特网地址 (a structure for historical reasons) */ struct in_addr

{

unsigned long s_addr;

};

如 果 你 声 明 了 一 个 " ina " 作 为 一 个 struct sockaddr_in 的 结 构 , 那 么

"ina.sin_addr.s_addr"就是 4 个字节的 IP 地址(按网络字节顺序排放)。需要注意的是,即使你的系统仍然使用联合而不是结构来表示 struct in_addr,你仍然可以用上面的方法得到 4 个字节的

IP 地址(一些 #define 帮了你的忙)。


8.4.2 基本转换函数

在前面提到了网络字节顺序。那么什么是网络字节顺序,它有什么特殊性,又如何将我们通常使用 的数据转换成这种格式呢?

1.网络字节顺序

因为每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,底位在后,而有的系统是底位在前,高位在后),而网络传输的数据大家是一定要统一顺序的。所以对与内部字节表示顺序和网络字节顺序不同的机器,就一定要对数据进行转换(比如 IP 地址的表示,端口号的表示)。但是内部字节顺序和网络字节顺序相同的机器该怎么办呢?

是这样的:它们也要调用转换函数,但是真正转换还是不转换是由系统函数自己来决定的。


2.有关的转化函数

我们通常使用的有两种数据类型:短型(两个字节)和长型(四个字节)。下面介绍的这些转换函数对于这两类的无符号整型变量都可以进行正确的转换。

如果你想将一个短型数据从主机字节顺序转换到网络字节顺序的话,有这样一个函数:它是以"h" 开头的(代表"主机") ;紧跟着它的是"to" ,代表"转换到" ;然后是"n"代表"网络" ; 最后是"s" ,代表"短型数据"。H-to-n-s,就是 htons() 函数(可以使用 Host to Network Short 来助记)

很简单吧……我没有理解的时候觉得这个函数不好记呢……

你可以使用 "n", "h", "to", "s", "l"的任意组合……当然,你要在可能的情况下进行组合。比如,系统是没有 stolh() 函数的(Short to Long Host?)。

下面给出套接字字节转换程序的列表:

· htons()——"Host to Network Short"

注意:现在你可能认为自己已经精通于这几个函数的用处了……你可能会想: "恩……在我的

68000 机器内部,字节的表示顺序已经是网络字节顺序了,那么我的程序里就不必调用 htonl() 来转换我的 IP 地址了" 。是的,你可能是对的。但是假如你把你的程序移植到一个内部字节顺序和网络字节顺序相反的机器上,你的程序就会运行不正常!所以,一定要记住:在你把数据发送到 Internet 之前,一定要把它的字节顺序从主机字节顺序转换到网络字节顺序!

在 struct sockaddr_in 中的 sin_addr 和 sin_port 他们的字节顺序都是网络字节顺序,而

sin_family 却不是网络字节顺序的。为什么呢?


这个是因为 sin_addr 和 sin_port 是从 IP 和 UDP 协议层取出来的数据,而在 IP 和 UDP 协议层, 是直接和网络相关的,所以,它们必须使用网络字节顺序。然而,sin_family 域只是内核用来判断struct sockaddr_in 是存储的什么类型的数据,并且,sin_family 永远也不会被发送到网络上,所以可以使用主机字节顺序来存储。


3.IP 地址转换

很幸运, Linux 系统提供和很多用于转换 IP 地址的函数,使你不必自己再写出一段费力不讨好的子程序来吃力的变换 IP。

首先,让我假设你有一个 struct sockaddr_in ina,并且你的 IP 是 166.111.69.52 ,你想把你的 IP 存储到 ina 中。你可以使用的函数:inet_addr() ,它能够把一个用数字和点表示 IP 地址的字符串转换成一个无符号长整型。你可以像下面这样使用它:

ina.sin_addr.s_addr = inet_addr(" 166.111.69.52" ); 注意:

· inet_addr()返回的地址已经是网络字节顺序了,你没有必要再去调用 htonl() 函数,是不是很方便呢?

· 上面的用法并不是一个很好的习惯,因为上面的代码没有进行错误检查。如果 inet_addr() 函数执行错误, 它将会返回 – 1…… 等等! 二进制的无符号整数值– 1 相当于什么? 相当于255.255.255.255 !! 一个广播用的 IP 地址!没有办法,你只能在你自己的程序里进行对症下药的错误检查了。

好,现在我们已经可以把字符串的 IP 地址转换成长整型了。那么还有没有其他的方法呢?如果你有一个 struct in_addr 并且你想把它代表的 IP 地址打印出来(按照 数字.数字.数字.数字的格式)……

这里,你可以使用函数 inet_ntoa()( "ntoa"代表"Network to ASCII"): printf(" %s" , inet_ntoa(ina.sin_addr));


这段代码将会把 struct in_addr 里面存储的网络地址以 数字.数字.数字.数字 的格式显示出来。


注意:

· inet_ntoa() 使用 struct in_addr 作为一个参数,不是一个长整型值。

· inet_ntoa() 返回一个字符指针,它指向一个定义在函数 inet_ntoa() 中的 static 类型字符串。所以每次你调用 inet_ntoa(),都会改变最后一次调用 inet_ntoa() 函数时得到的结果。

比如:

char *a1, a2;

a1 = inet_ntoa(ina1.sin_addr); /* this is 166.111.69.52 */ a2 = inet_ntoa(ina2.sin_addr); /* this is 166.111.69.53 */ printf("address 1: %s\\n" ,a1);

printf("address 2: %s\\n" ,a2); 将会显示出:

address 1: 166.111.69.53

address 2: 166.111.69.53

如果你想把结果保存下来,那么你可以在每次调用 inet_ntoa() 后调用 strcpy() 将结果存到另 外一个你自己的字符串中。

在后面,将会介绍怎样把域名转换为 IP。


8.5 基本套接字调用

Linux 支持伯克利(BSD)风格的套接字编程.它同时支持面向连接和不连接类型的套接字。

在面向连接的通讯中服务器和客户机在交换数据之前先要建立一个连接.再不连接通讯中数据被作 为信息的一部分被交换.无论那一种方式,服务器总是最先启动,把自己绑定(Banding)在一个套接字上,然后侦听信息.服务器究竟怎样试图去侦听就得依靠你编程所设定的连接的类型了。

你需要了解的一些系统调用:

· socket()

· bind()

· connect()

· listen()

· accept()

· send()

· recv()

· sendto()

· recvfrom()

· close()

· shutdown()

· setsockopt()

· getsockopt()

· getpeername()

· getsockname()

· gethostbyname()

· gethostbyaddr()

· getprotobyname()

· fcntl()

我们将在以下详细介绍这些系统调用。


8.5.1 socket()函数

取得套接字描述符!(记得我们以前说过的吗?它其实就是一个文件描述符)

socket 函数的定义是下面这样子的:

#include

#include

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


你是否对 int domain 和 int type、int protocol 有些疑惑呢?调用 socket()的参数是什么呢? 首先,domain 需要被设置为 "AF_INET" ,就像上面的 struct sockaddr_in。然后,type 参数告诉内核这个 socket 是什么类型,"SOCK_STREAM"或是"SOCK_DGRAM" 。最后,只需要把 protocol

设置为 0。


注意: 事实上, domain 参数可以取除了"AF_INET" 外的很多值,types 参数也可以取除了

"SOCK_STREAM"或"SOCK_DGRAM"的另外类型。具体可以参考 socket 的 man pages(帮助页)。


套接字创建时没有指定名字.客户机用套接字的名字读写它。这就是下面的绑定函数所要做之事。


socket() 函数只是简单的返回一个你以后可以使用的套接字描述符。如果发生错误,socket() 函数返回 – 1。全局变量 errno 将被设置为错误代码。(可以参考 perror() 的 man pages)


8.5.2 bind()函数

bind() 函数可以帮助你指定一个套接字使用的端口。

当你使用 socket() 函数得到一个套接字描述符,你也许需要将 socket 绑定上一个你的机器上的端口。

· 当你需要进行端口监听 listen()操作,等待接受一个连入请求的时候,一般都需要经过这一步。比如网络泥巴(MUD),Telnet a.b.c.d 4000。

· 如果你只是想进行连接一台服务器,也就是进行 connect() 操作的时候,这一步并不是必须的。


#include

#include

int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ; 参数说明:

· sockfd 是由 socket()函数返回的套接字描述符。

· my_addr 是一个指向 struct sockaddr 的指针, 包含有关你的地址的信息:名称、端口和 IP 地址。

· addrlen 可以设置为 sizeof(struct sockaddr)。


好,下面我们看一段程序:


#include <string.h>

#include

#include

#define MYPORT 4000 int main()

{

int sockfd ;

struct sockaddr_in my_addr ;

sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 在你自己的程序中 */

/* 要进行错误检查!! */

my_addr.sin_family = AF_INET ; /* 主机字节顺序 */ my_addr.sin_port = htons(MYPORT); /* 网络字节顺序,短整型 */ my_addr.sin_addr.s_addr = inet_addr(" 166.111.69.52") ; bzero(&(my_addr.sin_zero), 8); /* 将整个结构剩余*/

/* 部分数据设为 0 */

/* 不要忘记在你自己的程序中加入判断 bind 错误的代码*/ bind(sockfd,(struct sockaddr *)&my_addr,

sizeof(struct sockaddr));

……

……

}

这里有一些值得注意的代码段:


· my_addr.sin_port 是网络字节顺序。

· my_addr.sin_addr.s_addr 也是网络字节顺序。


· 代码段包含的头文件,在不同的系统中可能有一点小小的区别。(不过在 Linux 中是如此)如果并非如此,你可以查一查 man pages 来获取帮助。

最后,bind() 可以在程序中自动获取你自己的 IP 地址和端口。代码如下:

my_addr.sin_port = 0 ; /* 随机选择一个端口 */

my_addr.sin_addr.s_addr = INADDR_ANY ; /* 使用自己的地址 */


如上,通过设置 my_addr.sin_port 为 0,bind()可以知道你要它帮你选择合适的端口;通过设置

my_addr.sin_addr.s_addr 为 INADDR_ANY,bind() 知道你要它将 s_addr 填充为运行这个进程的机器的 IP。这一切都可以要求 bind() 来自动的帮助你完成。

如果你注意到了一些细节的话,你可能会发现我并没有将 INADDR_ANY 转换为网络字节顺序!是这样的,INADDR_ANY 的值为 0,0 就是 0,无论用什么顺序排列位的顺序,它都是不变的。

有读者会想了,因为我用的 INADDR_ANY 是一个#define,那么如果将我的程序移植到另外一个系统,假如那里的 INADDR_ANY 是这样定义的:#define INADDR_ANY 100,那么我的程序不是就会不运行了吗?那么下面这段代码就 OK 了。

my_addr.sin_port = htons(0); /* 随机选择一个未用的端口 */ my_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*使用自己的 IP 地址 */

现在我们已经是这么的严谨,对于任何数值的 INADDR_ANY 调用 bind 的时候就都不会有麻烦了。当 bind() 函数调用错误的时候,它也是返回– 1 作为错误发生的标志。errn 的值为错误代码。另外一件必须指出的事情是:当你调用 bind() 的时候,不要把端口数设置的过小!小于 1024 的

所有端口都是保留下来作为系统使用端口的,没有 root 权利无法使用。你可以使用 1024 以上的任何

端口,一直到 65535 :你所可能使用的最大的端口号(当然,你还要保证你所希望使用的端口没有被其他程序所使用)。

最后注意有关 bind() 的是:有时候你并不一定要调用 bind() 来建立网络连接。比如你只是想连接到一个远程主机上面进行通讯,你并不在乎你究竟是用的自己机器上的哪个端口进行通讯(比如

Telnet),那么你可以简单的直接调用 connect() 函数,connect() 将自动寻找出本地机器上的一个未使用的端口,然后调用 bind() 来将其 socket 绑定到那个端口上。


8.5.3 connect()函数


让我们花一点时间来假设你是一个 Telnet 应用程序。你的使用者命令你建立一个套接字描述符。你遵从命令,调用了 socket()。然后,使用者告诉你连接到 "166.111.69.52"的 23 端口(标准的Telnet 端口)……你应该怎么做呢?


你很幸运:Telnet 应用程序,你现在正在阅读的就是套接字的进行网络连接部分:connect()。


connect() 函数的定义是这样的:

#include

#include

int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);


connect()的三个参数意义如下:

· sockfd :套接字文件描述符,由 socket()函数返回的。

· serv_addr 是一个存储远程计算机的 IP 地址和端口信息的结构。

· addrlen 应该是 sizeof(struct sockaddr)。下面让我们来看看下面的程序片段:

#include <string.h>

#include

#include

#define DEST_IP " 166.111.69.52"

#define DEST_PORT 23 int main()

{

int sockfd ;

/* 将用来存储远程信息 */ struct sockaddr_in dest_addr ;

/* 注意在你自己的程序中进行错误检查*/ sockfd = socket(AF_INET, SOCK_STREAM, 0);

/* 主机字节顺序 */ dest_addr.sin_family = AF_INET ;

/* 网络字节顺序,短整型 */

dest_addr.sin_port = htons(DEST_PORT); dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);

/* 将剩下的结构中的空间置 0 */ bzero(&(dest_addr.sin_zero), 8);

/* 不要忘记在你的代码中对 connect()进行错误检查*/ connect(sockfd, (struct sockaddr *)&dest_addr,

sizeof(struct sockaddr));

……

……

}

再次强调,一定要检测 connect() 的返回值:如果发生了错误(比如无法连接到远程主机,或是

远程主机的指定端口无法进行连接等)它将会返回错误值 – 1。全局变量 errno 将会存储错误代码。另外,注意我们没有调用 bind() 函数。基本上,我们并不在乎我们本地用什么端口来通讯,是不

是?我们在乎的是我们连到哪台主机上的哪个端口上。Linux 内核自动为我们选择了一个没有被使用的

本地端口。


在面向连接的协议的程序中,服务器执行以下函数:

· 调用 socket() 函数创建一个套接字。

· 调用 bind() 函数把自己绑定在一个地址上。

· 调用 listen() 函数侦听连接。

· 调用 accept() 函数接受所有引入的请求。

· 调用 recv() 函数获取引入的信息然后调用 send() 回答。


8.5.4 listen()函数


listen() 函数是等待别人连接,进行系统侦听请求的函数。当有人连接你的时候,你有两步需要做:通过 listen() 函数等待连接请求,然后使用 accept() 函数来处理。(accept() 函数在下面介绍)。

listen() 函数调用是非常简单的。函数声明如下:


#include

int listen(int sockfd, int backlog);


listen() 函数的参数意义如下:

· sockfd 是一个套接字描述符,由 socket()系统调用获得。

· backlog 是未经过处理的连接请求队列可以容纳的最大数目。

backlog 具体一些是什么意思呢?每一个连入请求都要进入一个连入请求队列,等待 listen 的程序调用 accept()(accept() 函数下面有介绍)函数来接受这个连接。当系统还没有调用 accept()函数的时候,如果有很多连接,那么本地能够等待的最大数目就是 backlog 的数值。你可以将其设成 5 到

10 之间的数值(推荐)。


像上面的所有函数一样, listen() 如果返回 – 1 ,那么说明在 listen() 的执行过程中发生了错误。全局变量 errno 中存储了错误代码。

那么我们需要指定本地端口了,因为我们是等待别人的连接。所以,在 listen() 函数调用之前, 我们需要使用 bind() 函数来指定使用本地的哪一个端口数值。

如果你想在一个端口上接受外来的连接请求的话,那么函数的调用顺序为:

socket() ;

bind() ;

listen() ;

/* 在这里调用 accept()函数 */


……


下面将不给出例程,因为 listen() 是非常容易理解的。下面的 accept() 函数说明中的例程中, 有 listen() 的使用。


8.5.5 accept()函数


函数 accept()有一些难懂。当调用它的时候,大致过程是下面这样的:


· 有人从很远很远的地方尝试调用 connect() 来连接你的机器上的某个端口

(当然是你已经在 listen() 的)。


· 他的连接将被 listen 加入等待队列等待 accept() 函数的调用(加入等待队列的最多数目由调用 listen() 函数的第二个参数 backlog 来决定)。

· 你调用 accept() 函数,告诉他你准备连接。

· accept() 函数将回返回一个新的套接字描述符,这个描述符就代表了这个连接!

好,这时候你有了两个套接字描述符,返回给你的那个就是和远程计算机的连接,而第一个套接字 描述符仍然在你的机器上原来的那个端口上 listen()。

这时候你所得到的那个新的套接字描述符就可以进行 send() 操作和 recv()操作了。下面是 accept() 函数的声明:

#include

int accept(int sockfd, void *addr, int *addrlen);

accept() 函数的参数意义如下:


· sockfd 是正在 listen() 的一个套接字描述符。

· addr 一般是一个指向 struct sockaddr_in 结构的指针;里面存储着远程连接过来的计算机的信息(比如远程计算机的 IP 地址和端口)。

· addrlen 是一个本地的整型数值,在它的地址传给 accept() 前它的值应该是 sizeof(struct

sockaddr_in);accept() 不会在 addr 中存储多余 addrlen bytes 大小的数据。如果 accept() 函数在 addr 中存储的数据量不足 addrlen,则 accept() 函数会改变 addrlen 的值来反应这个情况。

读者现在应该想到:如果调用 accept() 失败的话,accept() 函数会返回 – 1 来表明调用失败, 同时全局变量 errno 将会存储错误代码。

下面我们来看一段程序片段:


#include <string.h>

#include

#include

/* 用户连接的端口号 */

#define MYPORT 4000

/* 等待队列中可以存储多少个未经过 accept()处理的连接 */

#define BACKLOG 10 int main()

{

/* 用来监听网络连接的套接字 sock_fd,用户连入的套接字使用 new_fd */ int sockfd, new_fd ;

/* 本地的地址信息 */ struct sockaddr_in my_addr ;

/* 连接者的地址信息 */

struct sockaddr_in their_addr ; int sin_size;

/* 记得在自己的程序中这部分要进行错误检查! */ sockfd = socket(AF_INET, SOCK_STREAM, 0) ;

/* 主机字节顺序 */ my_addr.sin_family = AF_INET ;

/* 网络字节顺序,短整型 */ my_addr.sin_port = htons(MYPORT) ;

/* 自动赋值为自己的 IP */ my_addr.sin_addr.s_addr = INADDR_ANY ;

/* 将结构中未使用部分全部清零 */ bzero(&(my_addr.sin_zero), 8) ;

/* 不要忘记在你自己的程序中下面的程序调用需要进行错误检测*/ bind(sockfd, (struct sockaddr *)&my_addr,

sizeof(struct sockaddr)); listen(sockfd, BACKLOG);

sin_size = sizeof(struct sockaddr_in);

new_fd = accept(sockfd, &their_addr, &sin_size);

……

……

}

注意:我们使用了套接字描述符 new_fd 用来进行所有的 send() 和 recv() 调用。如果你只想获

得一个单独的连接,那么你可以将原来的 sock_fd 关掉(调用 close()),这样的话就可以阻止以后的连接了。

在面向连接的通信中客户机要做如下一些事:

· 调用 socket() 函数创建一个套接字。

· 调用 connect() 函数试图连接服务。

· 如果连接成功调用 write() 函数请求数据,调用 read() 函数接收引入的应答。


8.5.6 send()和 recv()函数


这两个函数是最基本的,通过连接的套接字流进行通讯的函数。


如果你想使用无连接的用户数据报的话,请参考下面的 sendto() 和 recvfrom() 函数。


#include

#include

int send(int sockfd, const void *msg, int len, int flags);


send 的参数含义如下:

· sockfd 是代表你与远程程序连接的套接字描述符。

· msg 是一个指针,指向你想发送的信息的地址。

· len 是你想发送信息的长度。

· flags 发送标记。一般都设为 0(你可以查看 send 的 man pages 来获得其他的参数值并且明白各个参数所代表的含义)。

下面看看有关 send() 函数的代码片段: char *msg = " Hello! World!";

int len, bytes_sent;

……

……

len = strlen(msg);

bytes_sent = send(sockfd, msg, len, 0);

……

……

……

send()函数在调用后会返回它真正发送数据的长度。


注意:send() 所发送的数据可能少于你给它的参数所指定的长度!


因为如果你给 send() 的参数中包含的数据的长度远远大于 send() 所能一次发送的数据,则

send() 函数只发送它所能发送的最大数据长度,然后它相信你会把剩下的数据再次调用它来进行第二次发送。

所以,记住如果 send() 函数的返回值小于 len 的话,则你需要再次发送剩下的数据。幸运的是, 如果包足够小(小于 1K),那么 send() 一般都会一次发送光的。


像上面的函数一样,send() 函数如果发生错误,则返回 – 1,错误代码存储在全局变量 errno中。


下面我们来看看 recv() 函数。


函数 recv() 调用在许多方面都和 send() 很相似,下面是 recv() 函数的声明:

#include

#include

int recv(int sockfd, void *buf, int len, unsigned int flags);


recv()的参数含义如下:

· sockfd 是你要读取数据的套接字描述符。

· buf 是一个指针,指向你能存储数据的内存缓存区域。

· len 是缓存区的最大尺寸。

· flags 是 recv() 函数的一个标志, 一般都为 0(具体的其他数值和含义请参考 recv() 的man pages)。

recv() 返回它所真正收到的数据的长度。(也就是存到 buf 中数据的长度)。如果返回 –1 则代表发生了错误(比如网络以外中断、对方关闭了套接字连接等),全局变量 errno 里面存储了错误代码。

很简单,不是吗?现在你已经可以使用套接字连接进行网络发送数据和接受数据了!


Ya! 你现在已经成为了一个 Linux 下的网络程序员了!


8.5.7 sendto()和 recvfrom()函数

这两个函数是进行无连接的 UDP 通讯时使用的。使用这两个函数,则数据会在没有建立过任何连接的网络上传输。因为数据报套接字无法对远程主机进行连接,想想我们在发送数据前需要知道些什么 呢?

对了!是远程主机的 IP 地址和端口!


下面是 sendto()函数和 recvfrom()函数的声明:

#include

#include

int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);

和你所看到的一样,这个函数和 send()函数基本一致。


· sockfd 是代表你与远程程序连接的套接字描述符。

· msg 是一个指针,指向你想发送的信息的地址。

· len 是你想发送信息的长度。

· flags 发送标记。一般都设为 0。 (你可以查看 send 的 man pages 来获得其他的参数值并且明白各个参数所代表的含义)

· to 是一个指向 struct sockaddr 结构的指针,里面包含了远程主机的

IP 地址和端口数据。


· tolen 只是指出了 struct sockaddr 在内存中的大小sizeof(struct sockaddr)。

和 send() 一样,sendto() 返回它所真正发送的字节数(当然也和 send() 一样,它所真正发送的字节数可能小于你所给它的数据的字节数)。 当它发生错误的时候,也是返回– 1,同时全局变量

errno 存储了错误代码。


同样的,recvfrom() 函数和 recv() 函数也基本一致。


#include

#include

int recvfrom(int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen);

其参数含义如下:


· sockfd 是你要读取数据的套接字描述符。

· buf 是一个指针,指向你能存储数据的内存缓存区域。

· len 是缓存区的最大尺寸。

· flags 是 recvfrom() 函数的一个标志, 一般都为 0 (具体的其他数值和含义请参考 recvfrom() 的 man pages)。


· from 是一个本地指针,指向一个 struct sockaddr 的结构(里面存有源 IP 地址和端口数)。


· fromlen 是一个指向一个 int 型数据的指针,它的大小应该是sizeof(struct sockaddr).当函数返回的时候,

formlen 指向的数据是 form 指向的 struct sockaddr 的实际大小。


recvfrom() 返回它接收到的字节数,如果发生了错误,它就返回 – 1,全局变量 errno 存储了错误代码。

如果一个信息大得缓冲区都放不下,那么附加信息将被砍掉。该调用可以立即返回,也可以永久的 等待。这取决于你把 flags 设置成什么类型。你甚至可以设置超时(timeout)值。

在说明书(man pages)中可以找到 recvfrom 的更多信息。


注意:如果你使用 cnnect() 连接到了一个数据报套接字的服务器程序上,那么你就可以使用

send() 和 recv() 函数来传输你的数据。不要以为你在使用一个流式的套接字,你所使用的仍然是一个用户数据报的套接字,只不过套接字界面在 send() 和 recv()的时候自动帮助你加上了目标地址, 目标端口的信息。


8.5.8 close()和 shutdown()函数

程序进行网络传输完毕后,你需要关闭这个套接字描述符所表示的连接。实现这个非常简单,只需 要使用标准的关闭文件的函数:close()。

使 用 方 法 : close(sockfd);

执行 close()之后,套接字将不会在允许进行读操作和写操作。任何有关对套接字描述符进行读和写的操作都会接收到一个错误。

如果你想对网络套接字的关闭进行进一步的操作的话,你可以使用函数 shutdown()。它允许你进行单向的关闭操作,或是全部禁止掉。

#include

int shutdown(int sockfd, int how); 它的参数含义如下:

· sockfd 是一个你所想关闭的套接字描述符。

· how 可以取下面的值。0 表示不允许以后数据的接收操;1 表示不允许以后数据的发送操作;

2 表示和 close() 一样,不允许以后的任何操作(包括接收,发送数据)。


shutdown() 如果执行成功将返回 0,如果在调用过程中发生了错误,它将返回–1,全局变量 errno中存储了错误代码。

如果你在一个未连接的数据报套接字上使用 shutdown() 函数(还记得可以对数据报套接字 UDP 进行 connect() 操作吗?),它将什么也不做。


8.5.9 setsockopt()和 getsockopt()函数

Linux 所提供的 socket 库含有一个错误(bug)。此错误表现为你不能为一个套接字重新启用同一个端口号,即使在你正常关闭该套接字以后。例如,比方说,你编写一个服务器在一个套接字上等待的 程序。服务器打开套接字并在其上侦听是没有问题的。无论如何,总有一些原因(不管是正常还是非正 常的结束程序)使你的程序需要重新启动。然而重启动后你就不能把它绑定在原来那个端口上了。从

bind() 系统调用返回的错误代码总是报告说你试图连接的端口已经被别的进程所绑定。


问题就是 Linux 内核在一个绑定套接字的进程结束后从不把端口标记为未用。在大多数

Linux/UNIX 系统中,端口可以被一个进程重复使用,甚至可以被其它进程使用。


在 Linux 中绕开这个问题的办法是,当套接字已经打开但尚未有连接的时候用 setsockopt() 系统调用在其上设定选项(options)。而 getsockopt() 可以从给定的套接字取得选项。

这里是这些调用的语法:


#include

#include

int getsockopt(int sockfd, int level, int name,

char *value, int *optlen); int setsockopt(int sockfd, int level, int name,

char *value, int *optlen); 下面是两个调用的参数说明:

· sockfd 必须是一个已打开的套接字。

· level 是函数所使用的协议标准(protocol level) (TCP/IP 协议使用

PPROTO_TCP,套接字标准的选项实用 SOL_SOCKET)。


· name 选项在套接字说明书中(man page)有详细说明。

· value 指向为 getsockopt()函数所获取的值,setsockopt()函数所设置的值的地址。

· optlen 指针指向一个整数,该整数包含参数以字节计算的长度。

现在我们再回到 Linux 的错误上来.当你打开一个套接字时必须同时用下面的代码段来调用

setsockopt() 函数:


/* 设定参数数值 */


opt = 1; len = sizeof(opt);


/* 设置套接字属性 */ setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len);

setsockopt()函数还有很多其他用法,请参考帮助页(man pages)。


8.5.10 getpeername() 函数


这个函数可以取得一个已经连接上的套接字的远程信息(比如 IP 地址和端口),告诉你在远程和你连接的到底是谁。

#include


int getpeername(int sockfd, struct sockaddr *addr, int *addrlen); 下面是参数说明:

· sockfd 是你想取得远程信息的那个套接字描述符。

· addr 是一个指向 struct sockaddr (或是 struct sockaddr_in)的指针。

· addrlen 是一个指向 int 的指针,应该赋于 sizeof(struct sockaddr)的大小。


如果在函数执行过程中出现了错误,函数将返回 –1,并且错误代码储存在全局变量 errno 中。当你拥有了远程连接用户的 IP 地址,你就可以使用 inet_ntoa() 或 gethostbyaddr() 来输出

信息或是做进一步的处理。


8.5.11 gethostname()函数


gethostname() 函数可以取得本地主机的信息.它比 getpeername() 要容易使用一些。


它返回正在执行它的计算机的名字。返回的这个名字可以被 gethostbyname() 函数使用,由此可以得到本地主机的 IP 地址。

#include <unistd.h>

int gethostname(char *hostname, size_t size); 参数说明如下:

· hostname 是一个指向字符数组的指针,当函数返回的时候,它里面的数据就是本地的主机的名字。

· size 是 hostname 指向的数组的长度。

函数如果成功执行,它返回 0,如果出现错误,则返回–1,全局变量 errno 中存储着错误代码。


分享到:


相關文章: