首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Linux 网络编程—Socket

网络编程就是要设计一个能够通过网络和另一个进程进行通信的程序。

进程可以简单地理解为运行中的程序。

Socket 即套接字,能够唯一确定进行通信的两个进程。至于套接字是如何关联通信双方的,不必关心。套接字由系统维护。

每一个套接字都有一个唯一的编号,称为文件描述符。程序只需要保存好这个描述符就行了。通信过程中的任何操作都需要提供这个描述符,包括结束通信。

通信双方必须有一方扮演服务器角色,而另一方扮演客户端。客户端程序主动请求连接,而服务器程序被动接受连接。

服务器程序和客户端程序的工作流程不尽相同。本文一并介绍服务器和客户端在通信过程中的各个环节。对于服务器或客户端特有的环节,会特别说明。

创建套接字

套接字需要通过调用 函数创建。如果创建成功, 函数的返回值就是套接字的文件描述符,否则是

第一个参数用于说明地址族。通信的前提就是能够定位另一方。如果找不到另一方,通信就无从谈起。地址族取决于通信的情形,不同的通信情形对应不同的地址族。

进程间通信的情形大致可分为:本地的两个进程基于文件系统进行通信;两个进程通过 TCP/IP 体系结构的网络进行通信。其中 TCP/IP 就有 IPv4 和 IPv6 两种地址族。

AF 即 Address Family,INET 即 Internet

后面两个参数分别说明套接字的类型和协议。对于 TCP/IP,类型是和协议一一对应的。

从 Linux 内核 2.6.17 起,参数 还可以接受上述取值与 和 按位或的值。前者表示将 Socket 设为非阻塞;后者表示子进程中关闭该 Socket

下面代码首先定义一个整型变量 用来保存套接字的文件描述符,然后调用 函数创建一个基于 IPv4 寻址、使用面向字节流的 TCP 协议的套接字。

如果要使用的是 UDP 协议,则参数应改为:

应该在 函数之后使用一个 语句。这样才能在套接字创建失败时及时发现。使用 函数需要包含头文件

fd 即 file descriptor 文件描述符

套接字的地址

确定了套接字的地址族、类型和协议后,还要给出套接字要关联的地址。这相当于现实中,想要见面的两个人,约定见面的地方。

对于服务器程序,就是给出所在主机的 IP 地址,并指定一个端口号来代表自己。对于客户端程序,要说明服务器的 IP 地址和服务器程序使用的端口号。

地址结构

套接字的地址用一个结构体来组织。对于 TCP/IPv4,使用的结构类型如下:

sin 即 sockaddr_in,in 即 Internet

第一个成员是名为 的无符号短整型,含义同 函数的第一个参数,即地址族。

第二个成员是 16 位的无符号短整型,用来保存端口号。

第三个成员是一个 类型的结构,仅有一个 32 位的无符号整型成员,IP 地址就保存于此。

第四个成员的存在只是为了使整个结构的长度与结构 保持一致。本身没有实际意义,用 填充即可。

下面代码首先定义一个 类型的结构变量 ,使用 函数将其全部填 后,填写了地址族。

有专门的清 0 函数

使用 函数或 函数都需要包含头文件

网络字节序

在填写 IP 地址和端口号时,需要注意字节序。下面以整型为例讨论字节序。

一般,在内存中,整型占用 4 个字节;短整型占用 2 个字节;长整型占用 8 个字节。假设一个整数包含的字节的内存地址分别是 n、n+1、n+2、n+3……,则地址 n 就是这个整数的起始端。如果定义了一个指针指向这个整数,则指针的具体指向就是起始端。

大多数 CPU 都将最低位的字节存于起始端。例如,只有两个字节的短整型 ,其低位的字节 存于 n,而高位的字节 存于 n+1

这样的顺序称为小端字节序。反过来,把最高位存于起始端则为大端字节序。

1 个十六进制位相当于 4 个二进制位,8 个二进制位为 1 个字节。

向网络中传输字节时,是从起始端开始传输的。不过 TCP/IP 规定,传输的第一个字节是最高位,最后一个字节才是最低位。即 在内存中应当是这样子的:

CPU 在内存中存放字节的顺序称为主机字节序;字节在网络中的传输顺序称为网络字节序。TCP/IP 规定的网络字节序就是大端字节序,而主机字节序是不确定的,这就需要确保字节序列在发送之前已经转换成网络字节序。

头文件 提供了一组函数,专门对整型的字节序进行转换。

htons 即 host to network short,htons 即 host to network long

下面代码分别向结构变量 填写了 IP 地址和端口号。

是一个全 0 的 IP 地址常量,在头文件 中定义。

全 0 的 IP 地址表示本机拥有的任意 IP 地址。

IP 地址通常采用点分十进制表示。例如 这样一个字符串。对于这种形式的 IP 地址,可以使用 函数进行解析。

下面是一些网络编程中经常需要用到的函数。

与描述符绑定

这一步是针对服务器程序而言,客户端程序一般不需要此步骤。

填写好地址后,需要使用 函数将地址结构与描述符绑定。

要与之绑定的描述符;

要求给出一个 结构的地址。前面定义的结构是 类型,而不是 类型。因此,对其取址后,还需要进行强制类型转换;

实际上就是整型,要给出前一个参数——地址结构——的大小。

如果绑定成功, 函数将返回 ,否则返回

下面代码将结构 与描述符 绑定。

要检查 函数的返回值,这样一旦绑定失败,就能立即知道。

如果服务器在调用 函数之前,没有设置端口号,或者说设置为 0,那么在调用 函数时,内核就会从空闲的端口号中随机挑选一个,作为本机在此次通信中所使用的端口。这种情况下,通过 函数可以得到确切的端口号。

函数 会将套接字 绑定的地址信息写入 指向的地址结构。第三个参数 指向的 型变量在调用前,必须初始化为地址结构的大小,函数返回时,该变量会被设为地址信息的实际大小。

如果提供的地址结构太小,地址将被截断。在这种情况下, 指向的变量会被设定为一个超出正常范围的值。

下面代码应放在 函数调用之后,对端口号进行判断,如果是 0,则调用 函数以获取确切的值。

函数 获取的是本机的 IP 地址和端口号。

建立连接

在准备好地址结构之后,如果是面向字节流的套接字,需要建立连接才能进行通信。如果是面向数据报的套接字,则不需要。

服务器接受并处理连接

对于服务器程序来说,就是调用 函数通知底层协议,可以开始接收来自客户端的请求,并与之建立连接,即开始监听。陆陆续续建立的连接将形成一个队列,等待 函数调取。

是要开始监听的套接字的描述符;

是队列的最大长度。如果连接数达到这个值,往后的请求将被直接拒绝。比如设为 5,也可以使用常量 ,这将由系统决定队列的最大长度。

如果顺利, 函数将返回 ,否则返回

下面代码启动了对套接字 的监听。

接下来要做的就是调用 函数。

函数会等到有客户端接入时才返回。返回值是一个新的文件描述符。后续与该客户端的通信都要通过这个文件描述符进行。

函数还会将客户端的地址信息存放到第二个参数指向的地址结构,并在第三个参数指向的整型变量给出地址信息的实际长度。

调用 函数时,如果对客户端的地址信息感兴趣,必须将第三个参数指向的整型变量设为可接受的长度,也就是地址结构的长度;如果不感兴趣,可将第二、三个参数均设为

下面代码调用 函数,开始处理套接字 的队列。

客户端请求建立连接

对于客户端来说,是调用 函数主动去连接服务器的套接字。

要求一个套接字的描述符。连接建立后,与服务器的通信都要通过这个套接字进行;

指向一个包含服务器地址信息的地址结构;

给出第二个参数——地址结构——的大小。

如果顺利, 函数将返回 ,否则返回

下面代码调用 函数尝试与 指定的服务器程序建立连接。

调用 函数之后,客户端可调用 函数获取本机在此次通信中所使用的端口号,以及代表本机的 IP 地址。

传输字节流

TCP 连接一旦建立,不管是服务器,还是客户端,都会有一个接收缓存和发送缓存。发送缓存里放的是即将发送给对方的数据;接收缓存里放的是来自对方的数据。

函数 从接收缓存读取指定数量的字节; 函数将指定数量的字节推送到发送缓存。

这两个函数的第一个参数 都要求一个描述符,以说明通过哪个套接字收发字节。

函数从第二个参数 的指向开始,写入从接收缓存读取到的字节; 函数从第二个参数 的指向开始,读取字节到发送缓存。它们的第三个参数 都用于指定读写的字节数。

对于 函数,默认是只有发送完所有字节才会返回。对于 函数,默认是只要接收到字节就返回,哪怕只有一个字节。它们的最后一个参数 就是用来改变这种默认行为的,一般设为 ,表示保持默认。

如果读写成功,它们的返回值都是实际读写的字节数,否则返回 。如果返回的字节数是 ,说明对方关闭连接,应该结束通信,不能再收发字节了。

下面代码,调用 向套接字 发送字节。第二个参数给出了要发送的字节 ,但第三个参数指定只发送 5 个字节,因此另一方将只能接收到

下面代码,在 循环中调用 函数从套接字 读取字节到字符数组 ,读取到的字节数保存在整型变量 中。然后用 函数输出读取到的字节。

当接收到 时, 循环的条件就不成立,便跳出循环。

参数 的取值是一些宏。这些宏可以同时起作用,只需将它们进行按位或运算即可。

收发数据报

如果是 UDP 套接字,客户端在填写服务器的地址信息后,就可以调用 函数向服务器发送数据报;服务器在调用 函数之后,就可以调用 函数开始接收来自客户端的数据报。

由于 UDP 是面向无连接的,每次发送数据报都要给出目标地址;接收数据报时也要同时获取源地址,否则无法知道收到的数据报来自哪个客户端。

关闭连接

不管是 TCP 套接字还是 UDP 套接字,要结束通信,必须调用 函数来关闭连接。

实际上, 并不会立即把连接关闭,它只是把 的引用次数减 1。只有当引用次数为 0 时,才真正关闭连接。在多进程程序中,每创建一个子进程,就会使父进程中打开的 Socket 的引用次数加 1

如果想要立即关闭连接,则应该使用下面函数。

参数 决定 的行为,取值如下:

不能再进行读操作,已经在接收缓存中的数据会被丢弃

不能再进行写操作,已经在发送缓存中的数据会在真正关闭连接之前发送出去

不能再进行读操作,也不能再进行写操作

附录 A:一个完整的服务器程序

下面是一个完整的服务器程序。启动后,监听在 上,并将接收到的字节打印出来。如果收到 则关闭连接并结束退出。

假设上面代码保存在 中,用 命令编译,输出为 并运行。

这时程序将在终端中持续运行。可以在另一个终端中用 命令与它连接。

接着输入 并按下回车,可以看到前面的终端同步输出

输入 则会断开连接。

附录 B:一个完整的客户端程序

下面是一个完整的客户端程序。启动后,会尝试连接到 ,并将终端输入的字节发送过去。如果发送的是 则关闭连接并结束退出。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20201112A074HP00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券