概述:Linux 操作系统实际上是一个网络操作系统,系统管理的很大部分工作其实都和网络有关。

  • TCP/IP 协议
  • Linux 网络配置
  • 基本网络服务
  • 网络安全
  • Linux 下网络通信基本流程和操作

0x01 TCP/IP 协议

在信息化设备中,及刷机已经从单一使用发展到了集群使用。越来越多的应用领域需要计算机在一定的地理范围内联合起来进行集群工作,从而促进了计算机和通信技术的紧密结合,形成了计算机网络。

在计算机网络中,为了实现计算机之间的相互通信,有许多不同的通信规程和约定,这些规程和约定就是计算机网络协议。这就要求不同的计算机厂商在开发和研制自己的网络系统时需要遵循一个统一的约定。网络标准化组织在 1981 年颁布了开放系统互联参考模型,即 ISO/OSI 模型,如下图所示,现在大部分网络协议都是参考这标准设计的。

TCP/IP 于 20 世纪 70 年代开始研究和开发,现在也广泛用于各种网络中,不论是局域网还是广域网都可以用 TCP/IP 协议来构造网络环境。同时 TCP/IP 协议也是 Unix、Linux 等操作系统中最重要的网络协议。以 TCP/IP 为核心协议的 Internet 更加促进了 TCP/IP 的应用和发展。

从体系结构上来看, TCP/IP 协议是 OSI 参考模型七层结构的简化,它只有应用层、传输层、网际层和网络接口层。其中:

  • TCP/IP 网络接口层对应于 OSI 参考模型的物理层和数据链路层;
  • TCP/IP 网际层对应于 OSI 参考模型的网络层;
  • TCP/IP 传输层包含 TCP、UDP 两个协议,对应于 OSI 参考模型的传输层;
  • TCP/IP 应用层包含了 OSI 参考模型的会话层、表示层、应用层;

TCP/IP 协议是由一组通信协议组成的协议簇,其核心部分包括传输层协议(TCP 和 UDP)、网络层协议(IP)和网络接口层,这三层部分的协议内容都在操作系统内核中实现。而应用层协议,如 FTP、HTTP 等协议都是通过网络编程接口与核心协议打交道,整个协议簇是一种分层结构,下层为上层服务,不同层次之间通过一些接口通信。

  1. 以太网

1980 年 9 月由美国 Xerox、Intel、DEC 公司合作第一次公布了 Ethernet 的物理层和数据链路层的技术规范,成为世界上第一个局域网工业标准。IEEE 802.3 国际标准是在 Ethernet 标准的基础上制定的。

以太网工作起来就像一个总线系统,每一台机器都通过一个分接器挂在一根很长的电缆上。为了让机器识别到自己生,每块以太网卡都有一个由制造商唯一分配的地址 MAC 地址。当一块以太网卡想要同另一块以太网卡对话时,它将向整个以太网电缆发送信息,其中包括自己的 MAC 地址和接收者的 MAC 地址。两块以太网卡试图在同一时间发送数据时,便会产生冲突。解决这种冲突的办法就是两台计算机取消这次发送各自等待一段随机时间再发送数据的尝试。

允许 Linux 和 TCP/IP 在以太网地址和 IP 地址间进行连接的是地址解析协议(ARP)缓存。当一台 Linux 机器开始在以太网上发消息时,它会通过 ARP 询问子网上其他机器的 MAC 地址。

  1. IP 协议

IP 协议位于 TCP/IP 协议簇的第三层,是 TCP/IP 协议的传输系统,也是整个 TCP/IP 协议簇的核心。 IP 协议负责数据报在计算机之间的寻址,决定数据传送到哪里以及出现网络故障时如何更换路由。IP 不保证传送的可靠性,在主机资源不足的情况下,它可能丢弃某些数据包,同时 IP 也不会检查数据链路层丢弃的报文。

1)IP 地址。TCP/IP 网络要进行通信,每一台主机都要预先分配一个唯一的 32 位地址作为该主机的标识符,这个主机必须使用该地址进行所有通信活动,这个地址也称为 IP 地址。IP 地址通常由网络标识 ID 和主机标识两部分组成,可以标识互联网络中任何一个网络中的任何主机。网络标识也称为网络地址,用于辨别子网,同一子网上的所有 TCP/IP 主机的网络 ID 都相同。主机标识也称为主机地址,用于辨别每一个网络中的主机。IP 地址格式如下图所示:

其中,网络 ID 是大于 1 位的二进制数,主机 ID 也是大于 1 位的二进制数。如计算中心网关地址 132.111.002.001 分解成协议所认识的地址就是:网络 ID(132.111)和主机 ID(002.001)

IP 地址分类如下:

  • A 类地址:网络类别字段占 1 位,即第 0 位为 0,表示是 A 类地址,第 17 位表示网络地址,第 831 位表示主机地址。它所表示的范围为 0.0.0.0~127.255.255.255,即能够表示 126 个网络地址,16387064 个主机地址。A 类地址通常用于超大型网络场合。
  • B 类地址:网络类别字段占 2 位,即第 0、1 位为 1 0 ,表示 B 类地址,第 215 位表示网络地址,第 1631 位表示主机地址,它所能表示的范围为 128.0.0.0~191.255.255.255,即能够表示 16256 个网络地址,64576 个主机地址。B 类地址通常用于大型网络的场合。
  • C 类地址:网络类别字段占 3 位,即第 0、1、2 位为 110 ,表示 C 类地址,第 323 位表示网络地址,第 2431 位表示主机地址。它所能表示的范围 192.0.0.0~192.233.255.255,即能够表示 2064512 个网络地址,254 个主机地址。C 类地址通常用于校园网或企业网。
  • 此外,还有 D 类和 E 类 IP 地址。 D 类地址用于多址广播地址,供特殊协议向选定的节点发送信息用。E 类地址暂时保留。

2)子网掩码。通常将一个较大网络分为多个较小的网络,每个小网络使用不同的网络 ID ,这样的小网络称为子网。

在网络通信时,若想要找到子网,需要定义子网掩码。子网掩码与 IP 地址一样也是一个 32 位的值,将它与主机的 IP 地址做按位 “与” 运算,可以屏蔽一部分 IP 地址,从而确定出这个网络地址。子网掩码可概括两个功能:用于区分网络地址和主机地址;用于将网络分割为多个子网。

  1. TCP 协议

TCP 协议是一种面向连接的、可靠的传输层协议。面向连接是指一次正常的 TCP 传输需要通过在 TCP 客户端和 TCP 服务端之间建立特定的虚电路连接来完成,这个过程通常称为“三次握手”,可靠的传输协议可避免数据传输错误。TCP 协议可以支持许多高层协议,它对高层协议的数据结构没有任何要求,对于 TCP 来说只是将这些传输的数据结构作为一种连续的数据流。

  1. UDP 协议

UDP 协议也是常用的传输层协议,提供非面向连接的、不可靠的数据流传输服务。这种服务不确定报文是否到达,不对报文排序,也不进行流量控制,因此 UDP 报文可能出现丢失、重复和失序等现象。与 TCP 相同的特点,UDP 协议也可以通过端口号支持多路复用功能。UDP 是一种简单的协议机制,通信开销小,效率高,适合对于可靠性要求不高当需要快捷、低延迟通信的应用场合。

0x02 配置Linux网络

在 Linux 系统中,TCP/IP 网络是通过若干文本文件进行配置的,通过了解和修改这些配置文件就能够完成 Linux 联网功能。下面将逐个介绍 Linux 网络配置相关的文件。

  1. /etc/HOSTNAME 文件

该文件包含系统的主机名称和完全的域名,如:linux.cic.tsinghua.edu.cn(Linux 为主机名,cic.tsinghua.edu.cn 为域名)

  1. /etc/host.conf 文件

该文件指定如何解析主机名。Linux 通过解析器库获得主机名对应的 IP 地址,内容如下:

1
2
3
order bind,hosts    # 指定主机名查询顺序,这里优先使用 DNS 解析域名,然后查询 /etc/hosts 文件
multi on # 表示 /etc/hosts 文件中指定的主机可以有多个地址
nospoof on # 指不允许对该服务器进行 IP 欺骗
  1. /etc/services 文件

/etc/services 文件中包含了服务名和端口号之间的映射,不少系统程序要使用这个文件,内容如下:

1
2
3
4
5
6
tcpmux 1/tcp    #TCP port service multiplexer
echo 7/tcp
echo 7/udp
discard 9/tcp sink null
discard 9/udp sink null
systatll/tcp users
  1. /etc/sysconfig/network 文件

该文件用来指定服务器上的网络配置信息,包含控制与网络有关的文件和守护程序行为的参数,例如:

1
2
3
4
5
6
NETWORKING=yes      #网络是否被配置
HOSTNAME=machine1
DOMAINNAME=cic.tsinghua.edu.cn # 分配给机器的域名
GATEWAY=210.34.6.2 #网关的 IP 地址
FORWARD_IPV4=yes #是否开启 IP 转发功能
GATEWAYDEV=eth0 #gw-dev表示网关的设备名
  1. /etc/sysconfig/network-scripts/ifcfg-ethN 文件
1
2
3
4
5
6
7
DEVICE="eth0"
IPADDR="192.168.2.1"
NETMASK="255.255.255.9"
NETWORK="192.168.2.0"
BROADCAST="192.168.2.255"
ONBOOT="yes"
BOOTPROTO="none"
  1. /etc/hosts 文件

/etc/hosts 文件包含了 IP 地址和主机名之间的映射,还包括主机名的别名。IP 地址的设计使计算机容易识别,但却很难记住它们,为了解决这个问题,创建了 /etc/hosts 文件,内容如下:

1
2
3
127.0.0.1 machanel localhost.localdomain localhost
192.168.1.100 machine7
192.168.1.101 otherpc otheralias
  1. /etc/resolv.conf 文件

该文件是由 DNS 客户端解析器(resolver 一个根据主机名解析 IP 地址的库)使用的配置文件,它包含主机的域名搜索顺序和 DNS 服务器的地址。示例如下:

1
2
3
search cic.tsinghua.edu.cn
nameserver 166.111.4.5
nameserver 166.111.8.28
  1. /etc/init.d/network 主机地址、子网掩码和网关

这个文件声明了 IP 地址、掩码、网络、广播地址和默认路由器的变量。示例如下:

1
2
3
4
5
IPADDR=192.168.1.100
NETMASK=255.255.255.0
NETWORK=192.168.1.0
BROADCAST=192.168.1.255
GATEWAY=192.168.1.1

0x03 域名服务器DNS

连接 TCP/IP 的每个网络接口用一个唯一的 32 位的 IP 地址标识,但由于数字比较复杂,难以记忆,而且没有形象性,因而发明了域名系统,在这种情况下,可以使用易于理解和较为形象的名称作为一台计算机的标识。大多数情况下,数字地址和域名地址可以交替使用,但无论用数字地址或是域名进行网络应用时,总是以 IP 地址为基础进行的。在网络进行连接前,系统必须将域名地址转换成 IP 地址,这就是 DNS 的任务。

大多数 DNS 服务器都使用 Bind。DNS 提供了从名字到 IP 地址的映射关系,这种映射关系不必是一一映射,一个 IP 可以有多个域名,一个域名也可以对应多个 IP。

0x04 Apache 服务器

Web 服务器是互联网上人马使用最多的一种服务器,它已经成了当今社会不可缺少的一种信息传播方式。目前市场上最流行的运行于 Linux 上的 Web 服务器是 Apache 服务器。

纵观 Apache,它为网络管理员提供了丰富多彩的功能,包括目录索引、目录别名、内容协商、可配置的 HTTP 错误报告、CGI 程序的 SetUID 执行、子进程资源管理、服务器端图像映射、重写 URL、URL 拼写检查以及联机手册 man 等。

0x05 邮件服务器

自从加州大学伯克利分校 U.C Berkeley 完成 sendmail 的最初版本以来,sendmail 受到了业界长久的重视,从 1983 年的 V5 版本一直到 1993 年改写的 V8 版本,都受到了人们的尊重。

sendmail 为 Linux 提供 SMTP 连接所需的服务,它对邮件信息进行分析并把它传送到目的地。sendmail 采用开放源代码的开发方式编写,其所需源码都可以免费得到并自由发布。它只提供了邮件路由功能,将发送留给管理员可以选择的本地代理。

0x06 samba 服务器

可以把 samba 当成是一个局域网络上的 file/printer sever,它可以提供文件系统、打印机或其他信息,并与 samba sever 在同一个子网域的 samba client 共享。

samba 是用来实现 SMB 的一种软件,它的工作原理是让 NETBIOS 和 SMB 这两个协议运行于 TCP/IP 通信协议之上,并使用 NETBEUI 协议让 Linux 计算机可以在网络上被其他计算机看到。所以,通过 samba 能够使得 不同系统的计算机可以在网络上进行沟通和共享文件。

0x07 Linux系统的文件安全

Linux 文件系统是由文件和目录组成的树形结构,每个文件目录包括文件类型、文件的存取权限、文件名、文件所有者、文件大小、文件建立及修改的日期等内容。任何一项内容遭受未授权的修改,文件的安全性都将遭受到破坏。保护文件系统的安全性应该从以下几个方面入手:

  • 文件存取权限的设置:文件属性决定了文件的被访问权限。
  • 设置用户 ID 和同组用户 ID 许可:用户 ID 许可设置和同组用户 ID 许可设置只给予可执行的目标文件。

0x08 用户口令安全

每个 Linux 的用户都拥有一个账号,通过登录到这个账号才能有限制地使用系统,而保护自身文件安全的唯一屏障就是口令,一旦这道屏障被破坏,整个系统的安全便无法得到保证。

Linux 系统中的 /etc/passwd 文件包含了全部系统需要知道的关于每个用户的信息。

0x09 防火墙技术

  1. 防火墙的概念

防火墙是一个矛盾统一体,它既要限制信息的流通,又要保持信息的流通。因此,根据网络安全性总体需求,防火墙可以遵循以下两种基本原则之一实现:

  • 一切未被允许的都是禁止的
  • 一切未被禁止的都是允许的

由于以上两种防火墙原则在安全性和可使用性上各有侧重,很多防火墙系统唉两者之间都采用了一定的折中。

  1. 防火墙的分类

根据不同的防范方式和侧重点,防火墙主要分为分组过滤型和代理服务型。它们在网络性能、安全性和应用透明性等方面各有利弊。

1)分组过滤型防火墙。分组过滤型防火墙只是根据数据包的内容来判断是否允许数据包的传输。通常在网路层上通过对分组中的 IP 地址、TCP/UDP 端口号以及协议状态等字段的检查来决定是否转发一个分组。

分组过滤型防火墙的基本原理:

  • 根据网络安全策略,在防火墙中事先设置分组过滤规则
  • 根据分组过滤规则,对经过防火墙的分组流进行检查
  • 分组过滤规则一定要按顺序排列。当一个分组到达时,按规则的排列顺序依次运用每个规则对分组进行检查。一旦分组与一个规则相匹配,则不再向下检查其他规则
  • 如果一个分组与一个拒绝转发的规则相匹配,则该分组将被禁止通过
  • 如果一个分组与一个允许转发的规则相匹配,则该分组将被允许通过
  • 如果一个分组不与任何规则相匹配,则该分组将被禁止通过,这是遵循 “一切未被允许的都是禁止的”原则

0x0a Linux 常用网络命令介绍

计算机网络的主要优点是能够实现资源和信息的共享,并且用户可以远程访问信息,Linux 提供了一组强有力的网络命令来为用户服务,这些工具能够帮助用户登录到远程计算机上,传输文件和执行远程命令等。

  1. ftp 命令

ftp 命令是标准文件传输协议的用户接口。能够运行用户传输 ASCII 文件和二进制文件。

  1. telnet 命令

用户使用 telnet 命令进行远程登陆。该命令运行用户使用 telnet 协议在远程计算机之间进行通信,用户可以通过网络在远程计算机上登录,就像登录到本地机器上执行命令一样。如下图所示:

  1. rlogin 命令

rlogin 是 remote login 的缩写。该命令与 telnet 命令类似,允许用户启动远程系统上的交互命令会话。如下图所示:

  1. rsh 命令

rsh 是 remote shell 的缩写。该命令在指定的远程主机上启动了一个 shell ,并执行用户在 rsh 命令行中指定的命令。如果用户没有给出要执行的命令,rsh 就用 rlogin 命令使用户登录到远程机上。

  1. rcp 命令

rcp 其实就通常使用的 scp 命令,是 remote file copy 的缩写。该命令用于在计算机之间复制文件。

0x0b Linux 下 TCP 通信实例

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <iostream>
#include <string>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>

//出错调用函数
void error_handle(std::string opt, std::string message)
{
//根据errno值获取失败原因并打印到终端
perror(opt.c_str());
std::cout << message << std::endl;
exit(1);
}

int main(int argc, char *argv[])
{
int serv_sock;
int client_sock;

struct sockaddr_in serv_addr;
struct sockaddr_in client_addr;

socklen_t client_addr_size;
char message[] = "hello world";

//判断参数数量,Usage: <port>, 需要在命令行输入服务器接收消息的端口号
if(argc < 2)
{
std::cout << "Usage : " << argv[0] << " <prot>" << std::endl;
exit(1);
}

//创建socket 套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock < 0)
{
error_handle("socket", "socket() error.");
}

//初始化套接字结构体
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//选择当前任意网卡
serv_addr.sin_port = htons(atoi(argv[1]));//设置接收消息的端口号

//绑定端口
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
{
error_handle("bind", "bind() error.");
}

//监听端口,设置等待队列数量为5
if(listen(serv_sock, 5) < 0)
{
error_handle("listen", "listen() error.");
}

//打印输出等待连接
std::cout << "Waiting Client...." << std::endl;

client_addr_size = sizeof(client_addr);
//等待接收客户端建立连接
client_sock = accept(serv_sock, (struct sockaddr*)&client_sock, &client_addr_size);
if(client_sock < 0)
{
error_handle("accept", "accept() error.");
}
//accept() 成功建立连接后,服务器就会得到客户端的 IP 地址和端口号。
//打印客户端 IP 和端口号
std::cout << "Client IP : " << inet_ntoa(client_addr.sin_addr) << " , port : " << ntohs(client_addr.sin_port) << std::endl;

//向客户端发送 "hello world" 消息,使用write 标准IO接口就可以
write(client_sock, message, sizeof(message));

//关闭TCP连接
close(client_sock);
//关闭socket套接字
close(serv_sock);

return 0;
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <string>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>

//出错调用函数
void error_handle(std::string opt, std::string message)
{
//根据errno值获取失败原因并打印到终端
perror(opt.c_str());
std::cout << message << std::endl;
exit(1);
}


int main(int argc, char *argv[])
{
int sock;//socket套接字
struct sockaddr_in serv_addr;//服务器套接字结构体
char message[64];//用于接收服务器消息的缓冲区
int str_len;

//判断当前参数数量,需要命令行参数 <IP> <port>
if(argc < 3)
{
std::cout << "Usage : " << argv[0] << " <IP> <port>" << std::endl;
exit(1);
}

//创建socket 套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
error_handle("socket", "socket() error.");
}

//初始化服务器套接字结构体参数,设置对方的IP地址和端口号
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));

//与服务器建立连接
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
{
error_handle("connect", "connect() error.");
}

//读取服务器发送来的消息
str_len = read(sock, message, sizeof(message) -1);
if(str_len < 0)
{
//read() 读取数据失败
error_handle("read", "read() error.");
}

//将读取到的输出打印出来
std::cout << "Recv Message : " << message << std::endl;

//关闭socket 套接字
close(sock);
return 0;

}

0x0c Linux 下 UDP 通信实例

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>
#include <string>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 128

//出错调用函数
void error_handle(std::string opt, std::string message)
{
//根据errno值获取失败原因并打印到终端
perror(opt.c_str());
std::cout << message << std::endl;
exit(1);
}

int main(int argc, char *argv[])
{
int serv_sock;
char message[BUF_SIZE];
int str_len;
socklen_t client_adr_sz;
struct sockaddr_in serv_adr, client_adr;

//因为服务器程序使用时通过命令行参数指定接收消息的端口号,
//所以当参数数量少于需求 2 时,程序结束并报错
if(argc != 2){
std::cout << "Usage : " << argv[0] << " <port>" << std::endl;
exit(0);
}

//创建socket 套接字,sock 用于后面发送和接收数据
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock == -1)
{
error_handle("socket", "UDP socket creation error.");
}

//初始化套接字结构体,初始化服务器套接字结构体中的 IP 地址和端口号
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));

//将socket套接字绑定固定端口进行消息接收
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handle("bind", "bind() error.");
}

//UDP 方式不需要进行 listen 和 accept 操作,直接就可以通过 recvfrom 函数去接收套接字中收到的数据。
while(1)
{
//清空接收消息数据的缓存区
memset(message, 0, BUF_SIZE);
//计算客户端结构体大小,用于接收客户端数据结构
client_adr_sz = sizeof(client_adr);
//recvfrom() 调用不仅收到对方发送来的数据,还通过最后两个参数返回了对方的 IP 地址和端口号信息,用于返回数据使用。
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&client_adr, &client_adr_sz);
//将收到的信息打印出来
std::cout << "Recv Message : " << message << std::endl;
//通过recvfrom 返回的发送方的IP地址和端口号信息,使用 sendto 将收到的信息再次发送回去。
sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&client_adr, client_adr_sz);
}

//结束程序后关闭socket套接字
close(serv_sock);

return 0;
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <string>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 128

//出错调用函数
void error_handle(std::string opt, std::string message)
{
//根据errno值获取失败原因并打印到终端
perror(opt.c_str());
std::cout << message << std::endl;
exit(1);
}

int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr, from_adr;

//因为客户端程序使用时通过命令行参数指定IP地址和端口号,
//所以当参数数量少于需求 3 时,程序结束并报错
if(argc < 3)
{
std::cout << "Usage : " << argv[0] << " <IP> <port>" << std::endl;
exit(0);
}

//创建socket 套接字,sock 用于后面发送和接收数据
sock = socket(PF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
error_handle("socket", "socket() error.");
}

//初始化套接字结构体,初始化服务器套接字结构体中的 IP 地址和端口号
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));

while(1)
{
//从标准输入获取发送数据,保存到message缓存区
std::cin >> message;
//判断如果输入是 'q' 或者 'Q' 则表示退出客户端程序
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
{
//退出
break;
}

//发送消息
sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));

//清空接收数组
memset(message, 0, BUF_SIZE);
//计算返回套接字结构体字节长度
adr_sz = sizeof(from_adr);
//接收UDP返回数据
str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);
//在接收数据尾部添加'0'结束符
message[str_len + 1] = 0;
//将接收数据打印输出
std::cout << "Recv Message : " << message << std::endl;

}
}