笔记:Linux环境C语言复习(16)// 网络

网络

  • 1. 网络相关概念
    • 1.1 协议与网络分层
      • 1.1.1 OSI模型与网际协议族
      • 1.1.2 TCP/IP协议族分层
    • 1.2 封装
    • 1.3 以太网帧
    • 1.4 分用
    • 1.5 端口号
    • 1.6 IP首部
    • 1.7 TCP数据与IP数据报关系
    • 1.8 TCP首部
    • 1.9 集线器、交换机、路由器,连接不同网帧的两个网络
    • 1.10 IP地址、子网掩码
      • 1.10.1 IP地址
      • 1.10.2 子网掩码
    • 1.11 局域网内、跨网段数据传输过程
    • 1.12 网关、路由表、ARP表概念
    • 1.12 相关命令
  • 2.TCP
    • 2.1 三次握手
    • 2.2四次分手
    • 2.3 基于TCP的编程模型
      • 2.3.1 socket(2)
      • 2.3.2 bind(2)
      • 2.3.3 listen(2)
      • 2.3.4 connect(2)
      • 2.3.5 accept(2)
      • 2.3.6 主机字节序与网络字节序的相互转换 - htonl(3)、htons(3)、ntohl(3)、ntohs(3)
      • 2.3.7 netinet/in.h 头文件
      • 2.3.8 inet_pton(3)
      • 2.3.9 inet_ntop(3)
    • 2.4 TCP服务器/客户端通讯举例
      • 2.4.1 服务器端编程流程
      • 2.4.2 客户端编程流程
      • 2.4.3 举例
  • 3. UDP
    • 3.1 基于UDP的编程模型
      • 3.1.1 recvfrom(2)
      • 3.1.2 sendto(2)
    • 3.2 UDP服务器/客户端通讯举例
      • 3.2.1 服务器端编程流程
      • 3.2.2 客户端编程流程
      • 3.2.3 举例

1. 网络相关概念

1.1 协议与网络分层

1.1.1 OSI模型与网际协议族

描述一个网络中各个协议层的常用方法是OSI模型,下图给出它与网际协议族的近似映射
笔记:Linux环境C语言复习(16)// 网络_第1张图片
OSI模型的底下两层是随系统提供的设备驱动程序和网络硬件
网络层由IPv4和IPv6这两个协议处理
可以选择的传输层有TCP和UDP。如上图,TCP和UDP之间留有间隙,表明网络应用可以绕过传输层直接使用IPv4或IPv6
OSI模型的顶上三层被合并为一层,称为应用层。这就是Web客户(浏览器)、Telnet客户、Web服务器、FTP服务器和其他我们在使用的网络应用所在的层。对于网际协议,OSI模型的顶上三层协议几乎没有区别

1.1.2 TCP/IP协议族分层

TCP/IP通常被认为是一个四层协议系统,每一层负责不同的功能:
笔记:Linux环境C语言复习(16)// 网络_第2张图片

  1. 链路层:有时也称作数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理与电缆的物理接口细节
  2. 网络层:有时也称为互联网层,处理分组在网络中的活动,例如分组的选路。在TCP/IP协议族中,网络层协议包括IP协议(网际协议),ICMP协议(Internet互联网控制报文协议),以及IGMP协议(Internet组管理协议)
  3. 运输层:主要为两台主机上的应用程序提供端到端的通信。在TCP/IP协议族中,有两个互不相同的传输协议:TCP(传输控制协议)和``UDP(用户数据报协议)```
    TCP为两台主机提供高可靠性的数据通信。它所做的工作包括把应用程序交给它的数据分成合适的小块交个下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等。由于运输层提供了高可靠性的端到端的通信,因此应用层可以忽略所有这些细节
    UDP为应用层提供一种非常简单的服务。它只是把称作数据报的分组从一台主机发送到另一台主机,但并不保证该数据报能到达一端。任何必要的可靠性必须由应用层来提供
  4. 应用层负责处理特定的应用程序细节。几乎各种不同的TCP/IP实现都会提供以下这些通用的应用程序:
    Telnet - 远程登录
    FTP - 文件传输协议
    SMTP - 简单邮件传送协议
    SNMP - 简单网络管理协议

1.2 封装

当应用程序用TCP传送数据是时,数据被送入协议栈中,然后逐个通过每一层直到被当作一串比特流送入网络。其中每一层对收到的数据都要增加一些首部信息(有时还要增加尾部信息),该过程如下图:
笔记:Linux环境C语言复习(16)// 网络_第3张图片
TCP传给IP的数据单元称为TCP报文段或TCP段(TCP segment)。IP传给网络接口层的数据单元称为IP数据报(IP datagram)。通过以太网传输的比特流称作网帧(Frame)

上图,网帧头和网帧尾下面标注的数字是典型以太网帧首部的字节长度
以太网数据帧的物理特性是其长度必须在46~1500字节之间

更准确地说,上图中IP和网络接口层之间传送的数据单元应该时分组(packet)。分组既可以是一个IP数据报,也可以是IP数据报的一个片(fragment)。

UDP数据与TCP数据基本一致。唯一的不同是UDP传给IP的信息单元称作UDP数据报(UDP datagram),而且UDP的首部长度为8字节

由于TCP、UDP、ICMP和IGMP都要向IP传送数据,因此IP必须在生成的IP首部中存入一个长度为8bit的数值,称作协议域。1表示为ICMP协议,2表示为IGMP协议,6表示为TCP协议,17表示为UDP协议

许多应用程序都可以使用TCP或UDP来传送数据。运输层协议在生成报文首部时要存入一个应用程序的标识符。TCP和UDP都用一个16bit端口号来表示不同的应用程序。TCP和UDP把原端口号和目的端口号分别存入报文首部中

网络接口分别要发送和接收IP、ARP和RARP数据,一次也必须在以太网的网帧首部中加入某种形式的标识,以指明生成数据的网络层协议。一次,以太网的网帧首部也有一个16bit的帧类型域

1.3 以太网帧

笔记:Linux环境C语言复习(16)// 网络_第4张图片
以太网帧采用48bit(6字节)的目的地址和源地址。这是硬件地址(MAC地址)

ARP和RARP协议对32bit(4字节)的IP地址和48bit(6字节)的MAC地址进行映射

以太网的2bit类型字段定义了后续数据的类型

类型字段后就是数据

4bitCRC字段用于帧内后续字节擦错的循环冗余码检验(检验和)(它也被称为FCS或帧检验序列)

1.4 分用

当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议盒都要去检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称为分用(Demultiplexing),该过程如下图:
笔记:Linux环境C语言复习(16)// 网络_第5张图片

1.5 端口号

TCP和UDP采用16bit的端口号来识别应用程序
服务器一般都是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP服务器的UDP端口号都是69。任何TCP/IP实现锁提供的服务都用知名的1~1023之间的端口号。
客户端通常对它所使用的端口号并不关心,只需保证该端口号在本机上是唯一的就可以了。客户端口号又称为临时端口号(即,存在时间很短暂)。这是因为它通常只是在用户运行该客户程序时才存在,而服务器则要主机开着,其服务就运行
大多数TCP/IP实现给临时端口分配1024~5000之间的端口号。大于5000的端口号为其他服务器预留的(Internet上并不常用的服务)

1.6 IP首部

IP数据报的格式如下图。普通的IP首部长为20个字节,除非含有选项字段
笔记:Linux环境C语言复习(16)// 网络_第6张图片
上图IP首部中,最高位在左边,记为0bit,最低位在右边,记为31bit
4个字节的32bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端(big endian)字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。其他形式存储二进制整数的机器,如小端(little endian)格式,则必须在传输数据之前把首部转换成网络字节序

4位版本:目前的协议版本号是4,因此IP又是也称为IPv4

4位首部长度:首部长度指的是首部占32bit字的数目(4个字节的段的数目)。由于它是一个4bit字段,因此首部最长位60个字节(60B = 15(4位能表示的最大数) * 4B)。普通IP数据报(没有任何选择项)字段的值是5(即,首部长度 :5 * 4B = 20B)

8位服务类型(TOS):服务类型(TOS)字段包括一个3bit的优先权子字段(现在已被忽略),4bit的TOS子字段和1bit未用位但必须置0。4bit的TOS分别代表:最小时延、最大吞吐量、最高可靠性和最小费用。4bit中只能置其中1bit。如果所有4bit均为0,那么就意味着一般服务

16位总长度(字节数):总长度字段是指整个IP数据报的长度,以字节为单位。利用首部长度字段和总长度字段,就可以知道IP数据报中数据内容的起始位置和长度。由于该字段长16bit,所以IP数据报最长可达65535字节

16位标识:标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1

8位生存时间(TTL):TTL(time to live)生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据报的生存时间。TTL的初始值由源主机设置(通常为32或64),一旦经过一个处理它的路由器,它的值就减去1。当该字段的值为0时,数据报就被丢弃,并发送ICMP报文通知源主机

8位协议:TCP、UDP、ICMP和IGMP都要向IP传送数据,因此IP必须在生成的IP首部中加入协议域,以表明数据属于哪一层。在8位协议中:1表示ICMP协议,2表示IGMP协议,6表示TCP协议,17表示UDP协议

16位首部检验和:首部检验和字段是根据IP首部计算的检验和码。它不对首部后面的数据进行计算。

1.7 TCP数据与IP数据报关系

笔记:Linux环境C语言复习(16)// 网络_第7张图片

1.8 TCP首部

笔记:Linux环境C语言复习(16)// 网络_第8张图片
16位源端、目的端端口号:每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TC连接
有时,一个IP地址和一个端口号称为一个插口(socket)。插口对(socket pair)可唯一确定互联网络中每个TCP连接的双方

32位序号:序号用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,择TCP用序号对每个字节进行计数。序号是32bit的无符号数,序号到达2^32 - 1后又从0开始

4位首部长度:首部长度给出首部中32bit字的数目(即,4B数据段的数目)。需要这个值是因为任选字段的长度是可变的。这个字段占4bit,因此TCP最多有60B的首部。当咩有任选字段,正常的长度是20B

保留6位:在TCP首部中有6个标志bit。他们中的多个课同时被设置为1,一下是6个标志的简介:
URG - 紧急指针有效
ACK - 确认序号有效
PSH - 接收方应该尽快将这个报文段交给应用层
RST - 重键连接
SYN - 同步序号用来发起一个连接
FIN - 发端完成发送任务

16位窗口大小:TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端期望接收的字节。窗口大小是一个16bit字段,因此窗口大小最大为65535字节

1.9 集线器、交换机、路由器,连接不同网帧的两个网络

网络中常用的设备:
Hub集线器 - 将电信号放大、分流,属于物理层交换
交换机 - 交换网帧,属于链路层交换
路由器 - 交换IP包,属于网络层交换

路由器通过交换交换IP包,实现两种不同网帧的网络的通信:
笔记:Linux环境C语言复习(16)// 网络_第9张图片

1.10 IP地址、子网掩码

1.10.1 IP地址

IPv4地址长32bit。Internet地址并不采用平面形式的地址空间。IP地址具有一定的结构,五类不同的互联网地址格式如下:
笔记:Linux环境C语言复习(16)// 网络_第10张图片
网络号 - 决定主机所属的网络
主机号 - 决定主机在网络中的编号

这些32bit的地址通常写成四个十进制的数,其中每个整数对应一个字节。这种表示方法称作"点分十进制表示法"。各类IPv4地址的范围:
笔记:Linux环境C语言复习(16)// 网络_第11张图片
有三类IP地址:

  1. 单播地址 - 目的端为单个主机
  2. 广播地址 - 目的端为给定网络上的所有主机
  3. 多播地址 - 目的端为同一组内的所有主机

1.10.2 子网掩码

子网掩码:用于找出IP地址中的网络号
子网掩码书写形式:192.168.1.130/25等价于192.168.1.130/255.255.255.128
IP地址与子网掩码作与操作,就可获取IP地址的网络号

1.11 局域网内、跨网段数据传输过程

参考一
参考二

1.12 网关、路由表、ARP表概念

参考

1.12 相关命令

arp -a:查看ARP表
route(ubuntu):查看路由表
netstat - nr(mac os):查看路由表
ping 目标IP地址/网址:查看本机与目标地址/网址的连接情况
ifconfig:查看内网IP
curl ifconfig.me:查看外网IP

2.TCP

2.1 三次握手

建立一个TCP连接时会发生下述情形

  1. 服务器必须准备好接受外来的连接。这通常通过调用socket、bind和listen这3个函数来完成,称为被动打开(passive open)
  2. 客户通过调用connect发起主动打开(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项
  3. 服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和客户SYN的ACK(确认)。
  4. 客户必须确认服务器的SYN。
    这种交换至少需要3个分组,因此称之为TCP的三次握手,下图为交换的3个分节
    笔记:Linux环境C语言复习(16)// 网络_第12张图片
    上图给出的客户的初始序列号为J,服务器的初始序列号为K。ACK中的确认号时发送这个ACK的一端所期待的下一个序列号。因为SYN占据一个字节的序列号空间,所以每个SYN的ACK中的确认号就是该SYN的初始序列号加1。类似的,每个FIN(表示结束)的ACK中的确认号为该FIN的序列号加1

2.2四次分手

TCP建立一个连接需要3个分节,终止一个连续则需要4个分节:

  1. 某个应用进程首先调用close,称该执行为主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕
  2. 接收到这个FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收
  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN
  4. 接收这个最终FIN的原发送端TCP(即,执行主动关闭的一端)确认这个FIN
    笔记:Linux环境C语言复习(16)// 网络_第13张图片
    类似SYN,一个FiN也占据1个字节的序列号空间。因此,每个FIN的ACK确认号就是这个FIN的序列号加1

2.3 基于TCP的编程模型

基于TCP的客户端/服务器通讯模型:
笔记:Linux环境C语言复习(16)// 网络_第14张图片

基于TCP的客户端/服务器编程模型:
笔记:Linux环境C语言复习(16)// 网络_第15张图片

2.3.1 socket(2)

socket - create an endpoint for communication
指定期望的通行协议类型(例如,使用IPv4的TCP、使用IPv6的UDP...)

所需头文件
       #include           /* See NOTES */
       #include 

函数原型
       int socket(int domain, int type, int protocol);

参数
domain - 指定网络层协议族
AF_INET:使用IPv4协议
AF_INET6:使用IPv6协议

type - 指定套接字类型
SOCK_STREAM:使用字节流套接字(即,适用于TCP)
SOCK_DGRAM:使用数据报套接字(即,适用于UDP使用)

protocol - 指定传输层与套接字一起使用的协议
IPPROTO_CP:使用TCP传输协议
IPPROTO_UDP:使用UDP传输协议
IPPROTO_SCTP:使用SCTP传输协议

返回值
成功,返回新的套接字描述符
失败,返回 -1,errno被设置

2.3.2 bind(2)

bind - bind a name to a socket
将通讯描述符与服务器地址空间绑定
把一个本地协议地址赋予一个套接字。对于国际网协议,协议地址是32位的IPv4地址或是128位的IPv6地址与16位的TCP或者UDP端口号的组合

所需头文件
       #include           /* See NOTES */
       #include 

函数原型
       int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);

参数
sockfd - socket(2)的返回值

addr - 指定一个指向特定于协议的地址结构的指针,地址结构定义如下:
           struct sockaddr {
               sa_family_t sa_family;
               char        sa_data[14];
           }
sa_family可选值:
AF_INET - IPv4
AF_INET6 - IPv6

addrlen - addr指定的地址结构的长度

返回值
成功,返回  0
失败,返回 -1

2.3.3 listen(2)

listen - listen for connections on a socket

所需头文件
       #include           /* See NOTES */
       #include 

函数原型
       int listen(int sockfd, int backlog);

参数
sockfd - socket(2)返回值

backlog - 最大的未决连接数

返回值
成功,返回  0
失败,返回 -1,errno被设置

2.3.4 connect(2)

connect - initiate a connection on a socket

所需头文件
       #include           /* See NOTES */
       #include 

函数原型
       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

参数
sockfd - socket(2)的返回值

addr - 服务器的地址空间

addrlen - addr指向地址空间大小

返回值
成功,返回  0
失败,返回 -1,errno被返回

2.3.5 accept(2)

accept - accept a connection on a socket

所需头文件
       #include           /* See NOTES */
       #include 

函数原型
       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数
sockdf - socket(2)返回值

addr - 客户端的地址空间填充addr指定的空间里
如果addr指定为NULL,那么addrlen也需要指定为NULL

addrlen - 客户端地址空间的尺寸

返回值
成功,返回连接描述符(使用这个连接描述符和客户端进行通讯)
失败,返回 -1,errno被设置

2.3.6 主机字节序与网络字节序的相互转换 - htonl(3)、htons(3)、ntohl(3)、ntohs(3)

htonl, htons, ntohl, ntohs - convert values between host and network byte order
主机字节序与网络字节序相互转化

所需头文件
       #include 

函数原型
       uint32_t htonl(uint32_t hostlong);

       uint16_t htons(uint16_t hostshort);

       uint32_t ntohl(uint32_t netlong);

       uint16_t ntohs(uint16_t netshort);

参数:
hostlong - 32位主机字节序数据
hostshort - 16位主机字节序数据
netlong - 32位网络字节序数据
netshort - 16位网络字节序数据

返回值
返回目标字节序的数据

2.3.7 netinet/in.h 头文件

netinet/in.h - Internet Protocol family
定义互联网协议族

所需头文件
		#include 

描述:
当<netinet/in.h>被包括,下面的类型是通过typedef定义的:
in_port_t - 用于定义一个精确为16位的无符号整数类型(定义端口号)
in_addr_t - 用于定义一个精确为32位的无符号整数类型(定义IP地址)

<netinet/in.h>头文件定义了in_addr结构体数据类型,其中至少包含以下成员:
struct in_addr{
	in_addr_t      s_addr;
};

<netinet/in.h>定义了sockaddr_in结构体数据类型,其中至少包含以下成员:
struct sockaddr_in{
	sa_family_t    sin_family;
	in_port_t      sin_port;
	struct in_addr sin_addr;
	unsigned char  sin_zero[8];
};

sockaddr_in结构体数据类型用于存储Internet协议族的地址。此类型的值必须转换为struct sockaddr结构体数据类型,以便与本文档中定义的套接字接口一起使用

<netinet/in.h>定义了sa_family_t数据类型,并在<sys/socket.h>中对其进行描述

<netinet/in.h>定义了以下宏作为getsockopt()和setsockopt()的level参数的值:
IPPROTO_IP - 虚拟IP
IPPROTO_ICMP - Control message protocol
IPPROTO_TCP - TCP
IPPROTO_UDP - UDP

<netinet/in.h>定义了以下宏作为connect()、sendmsg()和sendto()的目标地址:
INADDR_ANY - 主机地址
INADDR_BROADCAST - 广播地址

2.3.8 inet_pton(3)

inet_pton - convert IPv4 and IPv6 addresses from text to binary form
该函数将字符串src转换为af地址族中的网络地址结构,然后将网络地址结构复制到dst。af参数必须是AF_INET或AF_INET6。dst是按网络字节顺序写的

所需头文件
       #include 

函数原型
       int inet_pton(int af, const char *src, void *dst);

参数
af - 指定地址协议族类型
AF_INET - IPv4
AF_INET6 - IPv6

src - src指向一个字符串,该字符串包含一个点十进制格式的IPv4网络地址,“ddd.ddd.ddd.ddd”,其中ddd是一个范围为0到255的三位数。地址被转换为struct in_addr类型并复制到dst,它必须是sizeof(struct in_addr)字节长 

dst - 指向已转换为网络字节序的IP地址(以struct in_addr类型数据表示)的指针

返回值
返回 -1,表示af参数没有指定有效的地址协议族,errno被设置
返回  0,表示src参数表示的地址不符合af指定的地址协议族规定
返回  1,表示成功

2.3.9 inet_ntop(3)

inet_ntop - convert IPv4 and IPv6 addresses from binary to text form

所需头文件
       #include 

函数原型
       const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
此函数将af地址家族中的网络地址结构src转换为字符串。转换后的字符串被复制到dst指向的缓冲区中,该缓冲区必须是非空指针。调用者在参数size中指定此缓冲区中可用的字节数

参数
af - 指定地址协议族类型
AF_INET - 将src指向的struct in_addr(按网络字节顺序)类型数据转换成点分十进制格式的IPv4网络地址“ddd.ddd.ddd.ddd”。缓冲区dst必须至少有INET_ADDRSTRLEN字节长。

AF_INET6 - 将src指向的struct in6_addr(按网络字节顺序)类型数据转换成最合适的IPv6网络地址格式的表示。缓冲区dst必须至少有INET6_ADDRSTRLEN字节长。

src - 指定要转换的网络地址,IPv4对应structt in_addr,IPv6对应struct in6_addr

dst - 转换后的文本形式IP地址

size - 设置存放文本形式IP地址的空间大小,对IPv4至少指定为INET_ADDRSTRLEN,对IPv6至少指定为INET6_ADDRSTRLEN

返回值
成功,返回指向dst空间的首地址
失败,返回NULL,errno被设置

2.4 TCP服务器/客户端通讯举例

2.4.1 服务器端编程流程

  1. 创建服务器端,获取通讯描述符 - socket(2)
  2. 将通讯描述符和服务器的IP地址和端口号绑定 - bind(2)
  3. 监听通讯描述符 - listen(2)
  4. 阻塞等待客户端请求到来,获取连接描述符 - accept(2)
  5. 获取客户端的数据 - read(2)
  6. 数据处理
  7. 将处理后的数据发送给客户端 - write(2)
  8. 关闭和客户端的连接 - close(2)

2.4.2 客户端编程流程

  1. 创建客户端,获取通讯描述符 - socket(2)
  2. 与服务器连接 - connect(2)
  3. 向服务器发送数据 - write(2)
  4. 接收服务器的响应 - read(2)
  5. 关闭和服务器的连接

2.4.3 举例

需求:

  1. 客户端循环向服务器发送任意字符,服务器以大写形式返回客户端
  2. 客户端发送“quit”,客户端退出进程,服务器断开与客户端的连接
  3. 服务器并发处理多个客户端连接
  4. 服务器端显示连接的客户端IP号、Port号

MyServer.c 编译链接后生成可执行文件MyServer

#include   
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(void){
    int count;
    char IP[128];
    int sockfd,acptfd;
    pid_t pid;
    struct sockaddr_in server_sockaddr,client_sockaddr;
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(5017);
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    socklen_t len = sizeof(client_sockaddr);

    sockfd = socket(PF_INET,SOCK_STREAM,0);
    if(-1 == sockfd){
        perror("socket");
        return 2;
    }

    if(-1 == bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))){
        perror("bind");
        return 3;
    }

    if(-1 == listen(sockfd,10)){
        perror("listen");
        return 4;
    }

    while(1){
        acptfd = accept(sockfd,(struct sockaddr *)&client_sockaddr,&len);
        if(-1 == acptfd){
            perror("accept");
            return 5;
        }
        else{
            pid = fork();
            if(-1 == pid){
                perror("fork");
                return 6;
            }
            else if(0 == pid){
                printf("%s connected...\n",inet_ntop(AF_INET,&client_sockaddr.sin_addr,IP,128));    
                while(1){
                    int i = 0;
                    char writeBuf[128] = {0};
                    char readBuf[128] = {0};
                    count = read(acptfd,readBuf,128);
                    while('\n' != readBuf[i]){
                        writeBuf[i] = toupper(readBuf[i]);
                        i++;
                    }
                    if(0 == strcmp(readBuf,"QUIT\n"))
                        break;
                    write(acptfd,writeBuf,count);
                }
                close(acptfd);
                exit(0);
            }
            else{
                waitpid(-1,NULL,WNOHANG);
            }
        }
    }

    if(-1 == close(sockfd)){
        perror("close");
        return 7;
    }
    
    return 0;
}                                             

MyClient.c 编译链接后生成可执行文件MyClient

#include  
#include 
#include 
#include 
#include 
#include 

int main(int argc,char **argv){
    int count;
    struct sockaddr_in server_sockaddr;
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(5017);
    if(-1 == inet_pton(AF_INET,argv[1],&server_sockaddr.sin_addr)){
        perror("inet_pton");
        return 1;
    }   

    int sockfd = socket(PF_INET,SOCK_STREAM,0);
    if(-1 == sockfd){
        perror("socket");
        return 2;
    }   

    if(-1 == connect(sockfd,(struct sockaddr*)&server_sockaddr,sizeof(server_sockaddr))){
        perror("connect");
        return 3;
    }   
    else{
        printf("connected...\n");
    }   

    while(1){
        char readBuf[128] = {0};
        char writeBuf[128] = {0};
        printf("input strings or quit with \"quit\": ");
        fgets(writeBuf,128,stdin);
        write(sockfd,writeBuf,strlen(writeBuf) + 1); 
        if(0 == strcmp(writeBuf,"quit\n"))
            break;
        count = read(sockfd,readBuf,128);
        write(1,readBuf,count);
        printf("\n");
    }   
    if(-1 == close(sockfd)){
        perror("close");
        return 4;
    }
    else{
        printf("client process closed...\n");
    }
    
    return 0;
}                                                     

编译后生成MyServer、MyClient,开启MyServer后,在本地和网内使用客户端测试:
使用本地回环测试结果:

Desktop Linraffe$ MyClient 127.0.0.1
connected...
input strings or quit with "quit": giraffe
GIRAFFE
input strings or quit with "quit": quit
client process closed...

使用本地IP测试结果、使用网段内其他主机测试结果与本地回环相同

MyServer执行结果:

Desktop Linraffe$ MyServer
127.0.0.1 connected...
xxx.xxx.xxx.5 connected...
xxx.xxx.xxx.6 connected...

3. UDP

使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质差异,其原因在于两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。

3.1 基于UDP的编程模型

下图给出了典型的UDP客户/服务器程序的函数调用。客户不与服务器建立连接,而是只管使用sendto(2)函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。服务器不接受来自客户的连接,而是只管调用recvfrom(2)函数,等待来自某个客户的数据到达。recvfrom(2)将与所接收的数据报一起返回客户的协议地址,因此服务器可以把响应发送给正确的客户
笔记:Linux环境C语言复习(16)// 网络_第16张图片

3.1.1 recvfrom(2)

recvfrom - receive a message from a socket

所需头文件
       #include 
       #include 

函数原型
       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

参数
sockfd - socket(2)返回值

buf - 指向要接收数据的缓冲区地址

len - 要接收的字节数

flags - 0

src_addr - 指向一个将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构(指向发送者的协议地址空间,如果指定为NULL,addrlen也要指定为NULL)

addrlen - 指向存放将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构中填写的字节数的地址(指向src_addr变量空间的长度)

返回值
成功,返回接收到的字节数
失败,返回 -1,errno被设置

3.1.2 sendto(2)

sendto - send a message on a socket

所需头文件
       #include 
       #include 

函数原型
       ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

参数
sockfd - socket(2)返回值

buf - 指向要发送数据的缓冲区地址

len - 指定要发送的字节数

flags - 0

dest_addr - 指定要发送的目标地址

addrlen - 指定目标地址长度

返回值
成功,返回发送的字节数
失败,返回 -1,errno被设置

3.2 UDP服务器/客户端通讯举例

3.2.1 服务器端编程流程

  1. 创建通讯描述符 - socket(2)
  2. 将通讯描述符和服务器的地址空间绑定 - bind(2)
  3. 等待客户端数据到来 - recvfrom(2)
  4. 处理客户数据
  5. 回应客户端的消息 - sendto(2)

3.2.2 客户端编程流程

  1. 创建通讯描述符 - socket(2)
  2. 使用通讯描述符想服务器发送数据 - sendto(2)
  3. 等待服务器端响应信息 - recvfrom(2)
  4. 关闭通讯描述符 - close(2)

3.2.3 举例

MyServer.c 编译生成可执行文件MyServer

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(void){                                                                                                                                                                      
    char IP[128];
    struct sockaddr_in server_sockaddr,client_sockaddr;
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(5017);
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    socklen_t len = sizeof(client_sockaddr);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(-1 == sockfd){
        perror("socket");
        return 1;
    }   

    if(-1 == bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))){
        perror("bind");
        return 2;
    }   

    while(1){
        int count;
        int i = 0;
        char *recvBuf = (char *)malloc(sizeof(char) * 128);
        char *sendBuf = (char *)malloc(sizeof(char) * 128);
            
        count = recvfrom(sockfd,recvBuf,128,0,(struct sockaddr *)&client_sockaddr,&len);
        if(-1 == count){
            perror("recvfrom");
            return 3;
        }   
        else{
            printf("receive from:%s\n",inet_ntop(AF_INET,&client_sockaddr.sin_addr,IP,128));
            while(recvBuf[i]){
                sendBuf[i] = toupper(recvBuf[i]);
                i++;
            }   
        }   
            
        if(-1 == sendto(sockfd,sendBuf,count,0,(struct sockaddr *)&client_sockaddr,sizeof(client_sockaddr))){
            perror("sendto");
            return 4;
        }   
        
        free(recvBuf);
        free(sendBuf);
    
    }

    if(-1 == close(sockfd)){
        perror("close");
        return 5;
    }

    return 0;
}                                        

MyClient.c 编译生成可执行文件MyClient

#include                                                                                                                                                                    
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc,char **argv){
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    struct sockaddr_in server_sockaddr;
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(5017);
    char IP[128];

    if(-1 == inet_pton(AF_INET,argv[1],&server_sockaddr.sin_addr)){
        perror("inet_pton");
        return 1;
    }   

    if(-1 == sockfd){
        perror("socket");
        return 2;
    }   

    while(1){
        int count;
        int i = 0;
        char *recvBuf = (char *)malloc(sizeof(char) * 128);
        char *sendBuf = (char *)malloc(sizeof(char) * 128);
          
        printf("input a string or input a \"quit\" to quit:");
        fgets(sendBuf,128,stdin);

        if(0 == strcmp(sendBuf,"quit\n"))
            break;
        
        if(-1 == sendto(sockfd,sendBuf,strlen(sendBuf) + 1,0,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))){
            perror("sendto");
            return 3;
        }   

        count = recvfrom(sockfd,recvBuf,128,0,NULL,NULL);
        if(-1 == count){
            perror("recvfrom");
            return 4;
        } 
                else{
                write(1,recvBuf,count);
        }

        free(recvBuf);
        free(sendBuf);
    }
        

    if(-1 == close(sockfd)){
        perror("close");
        return 5;
    }

    return 0;
}                                        

在本地测试客户端:

Desktop Linraffe$ MyClient 127.0.0.1
input a string or input a "quit" to quit:giraffe
GIRAFFE

在内网测试客户端:

Linraffe@ubuntu:~/Desktop$ ./MyClient xxx.xxx.xxx.4
input a string or input a "quit" to quit:unicorn
UNICORN

服务器执行结果:

Desktop Linraffe$ MyServer
receive from:127.0.0.1
receive from:xxx.xxx.xxx.5

你可能感兴趣的:(笔记)