Linux下C语言编程之网络编程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/GDUTLYP/article/details/50214673


一、计算机网络体系概述

1、TCP/IP协议与OSI协议的体系结构

国际标准化组织ISO制定出了著名的开放系统互连基本参考模型OSI/RM,简称OSI模型。但是实际上,TCP/IP协议在事实上占有市场。所以TCP/IP成了事实上的通用标准。而OSI模型被保存下来,用于作为理解网络的一种参考。OSI的七层协议体系结构的概念清楚,理论也比较完善,但是它复杂又不实用。TCP/IP是一个四层的体系结构。为了便于理解,综合OSI和TCP/IP的优点,采用一种五层体系协议来解释。

以下从上至下对于网络各层的功能做一个简单的介绍。

(1)应用层

应用层是体系结构中的最高层。应用层直接为应用进程(程序)提供服务。这些服务按其向应用程序提供的特性分成组,并称为服务元素。有些可为多种应用程序共同使用,有些则为较少的一类应用程序使用。常用的应用层协议有如下几种:

DNS域名系统(Domain Name System)

功能:用于实现网络设备名字到IP地址映射的网络服务。

FTP文件传输协议(File Transfer Protocol)

功能:用于实现交互式文件传输功能。

SMTP简单邮件传送协议(Simple Mail Transfer Protocol)

功能:用于实现电子邮箱传送功能。

HTTP超文本传输协议(HyperText Transfer Protocol)

功能:用于实现WWW服务。

SNMP简单网络管理协议(simple Network Management Protocol)

功能:用于管理与监视网络设备。

Telnet远程登录协议

功能:用于实现远程登录功能。

(2)运输层

运输层的任务就是负责向两个主机中进程之间的通信提供服务。由于一个主机可以同时运行多个进程,因此运输层有复用和分用的功能。复用就是多个应用层的进程可以同时使用下面运输层提供的服务,分用则是运输层把收到的信息分别交付给上面应用层中相应的进程。

运输层主要使用一下两种协议:

传输控制协议(TCP):面向连接的,数据传输单位报文段,能够提供可靠的数据交付。

用户数据报协议(UDP):面向无连接的,数据传输的单位是用户数据报,不保证提供可靠的交付,只能尽最大努力交付。

(3)网络层

网络层负责为分组交换网上的不同主机提供通信服务。在发送数据时,网络层把运输层产生的报文段或者用户数据报封装成分组进行传送。在TCP/IP体系中,由于网络层使用IP协议,因此分组也叫作IP数据报,或简称为数据报

(4)数据链路层

常简称为链路层。数据链路可以粗略地理解为数据通道。物理层要为终端设备间的数据通信提供传输介质及其连接。介质是长期的,但是连接是有生存期的。在连接生存期内,收发两端可以进行一次或多次数据通信。每次通信都要经过建立通信联络和拆除通信联络两个过程。这种建立起来的数据收发关系就叫做数据链路。而在物理媒体上传输的数据难免受到各种不可靠因素的影响而产生差错,为了弥补物理层上的不足,为上层提供无差错的数据传输,就要能对数据进行检错和纠错。数据链路的建立,拆除,对数据的检错,纠错是数据链路层的基本任务。

两个主机之间的数据传输,总是在一段一段的链路上传送的,也就是说,在两个相邻结点之间(主机和路由器之间,或者两个路由器之间)传送数据的(点对点)。这时就需要使用的专门的链路层协议,在两个相邻的结点间传送数据时,数据链路层会把网络层交付下来的IP数据报组装成,然后在两个相邻的结点间的链路上“透明”地传输帧中的数据。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制信息等)。

所谓“透明”,就是指无论什么样的比特组合的数据都能够通过这个数据链路层。因此,对于数据来数,虽然数据链路层是存在的,但是数据感觉不到它。就像你无法看见一个在你面前100%透明的玻璃一样,虽然有,但不影响你看见玻璃后面的东西。

在接收到数据时,控制信息使接收端能够知道一个帧是从哪个比特开始和到哪个比特结束的。这样,数据链路层在接收到一个帧后,就可以从帧中提取出数据部分,上交给网络层。

控制信息还使得接收端能够检测出所收集到的帧中有无差错。如发现有错,数据链路层就简单的丢弃这个出错帧,以免浪费网络资源。如果需要改正错误,就由运输层的TCP协议来完成。

(5)物理层

物理层的任务就是透明的传送比特流。在物理层上所传送数据的单位是比特。也就是说,发送方发送1(或0)时,接受方应当受到1(或0)而不是0(或1)。因此物理层要考虑用多大的电压电表1或0,以及接受方如何辨识出发送方所发送的比特。物理层还要确定连接电缆的插头应该有多少根引脚以及各个引脚应如何链接。注意,传递信息所利用的一些物理媒体,如双绞线,同轴电缆,光缆等并不在物理层协议之内,而是在物理层的下面。因此也有人将物理媒体当做第0层。

2、计算机通信过程

下面简单总结一下应用程序的数据在各层之间的传递过程中所经历的变化。简单起见,假设连个主机是直接相连的。

图1-1 数据在各层之间的传递过程

 

假定主机1的应用进程APP1向主机2的应用进程APP2传送数据。APP1先将其数据交给本主机的第五层(应用层)。第五层加上必要的控制信息H5就变成了下一层的数据单元。第四层(运输层)收到这个数据单元后,加上本层的控制信息H4,再交给第三层(网络层),成为第三层的数据单元。以此类推。不过到了第二层(数据链路层)后,控制信息分成两部分,分别加在本层数据单元的首部(H2)和尾部(T2),而第一层(物理层)由于是比特流的传送,所以不再加上控制信息。注意:传送比特流时应该从首部开始传送。

当这一串比特流离开主机1经过网络的物理媒体传送到目的主机2时,就从主机2的第一层依次上升到第五层。每一层根据控制信息进行必要的操作,然后将控制信息剥去,将该层剩下的数据单元上交给更高一层。最后,把应用进程APP1发送的数据交给目的主机的应用进程APP2,完成一次通信。

二、TCP/IP协议

1、TCP/UDP简介

运输层提供了两种传送协议:传输控制协议(TCP)和用户数据报协议(UDP)。

传输控制协议(TransferControl Protocol,TCP)是一种面向连接的协议,网络程序在使用这个协议的时候,网络可以保证客户端和服务器端的链接是可靠的,安全的。

用户数据报协议(UserDatagram Protocol,UDP)是一种面向非连接的协议,这种协议并不能保证网络程序的链接是可靠的,所以一般采用TCP协议.

2、数据的封包与解包

应用程序所发送的数据称为有效载荷,为了有效载荷的顺利传送,运输层和网络层会添加一些控制信息,来保证传送的可靠性。其中与socket编程有关的最主要的项目有4个:

源主机的IP地址(源IP):使得目的主机能够得知源主机的IP,从而能够回送数据包。

源主机的端口号(源端口):用于标识员主机上发送数据的应用进程,以便将目的主机回传的数据交付给该进程。

目的主机的IP地址(目的IP):使数据包能够到达指定的目的主机,并被目的主机的网卡接收。

目的主机的端口号(目的端口):网卡接收到的数据包应该被目的主机上哪个应用进程接收和处理。

源主机会在数据包发送到网线上之前,使用上述4个内容对有效载荷进行封包操作:在有效载荷之前加上TCP包头(最主要的是目的端口源端口),然后再在前面加上IP包头(最主要的是目标IP源IP)。目的主机在接收到数据包之后,进行封包的逆操作来解包,提取有效载荷。

图2-1  TCP/IP数据包

注意:不同的网络应用程序占用不同的端口号,http的端口号为80,ftp的端口号为21,POP3的端口号为110,SMTP的端口号为25,TELNET的端口号为23等等。目的IP一般来源于应用程序的用户输入。一般来说,客户端程序不会固定的占用一个端口号,当它需要与别的机器进行通信时,会向操作系统请求并获得一个临时的源端口号。根据TCP/IP协议的规定,这个临时端口号是本机上尚未使用的大于1024(小于1024的端口被TCP/IP协议规定为专用的端口号,它们被各个著名的服务器所使用),小于65535(端口号由2个字节表示,最多为65535)的端口号。

3、TCP/UDP/IP包头简介

运输层和网络层按照固定的格式为有效载荷加上TCP/UDP包头和IP包头,下面分别介绍一下。

3.1  TCP包头的结构

       0                                    15 16                                 31

16位源端口号

16位目的端口号

32位序列号

32位确认序列号

4位数据偏移

保留

(6位)

U

R

G

A

C

K

P

S

H

R

S

T

S Y N

F I N

16位窗口大小

16位检验和

16位紧急指针

选项

数据

图2-2  TCP包头结构

TCP包头的各个字段的含义如下表2-1所示。

 

表2-1  TCP包头的含义

名称

作用

TCP源端口

记录源主机的端口号,源端口和源IP地址的作用是标识报的返回地址(即哪台主机的哪个进程)

TCP目的端口

记录目的主机的端口号,用来指明接受该报文的计算机上的应用程序的地址接口

TCP序列号

32位的序列号由接收端计算机使用,用来将分段的报文重组成最初的形式

TCP确认号

目的主机使用32位的应答域标识下一个希望收到的报文的第一个字节

数据偏移

指明TCP包头的大小,以32位数据结构(4字节)或“双字”为一个单位(包头后面就是有效载荷)

保留

该6位恒为0,为将来定义新的用途保留

标志位

6个标志位各控制一个功能,和上图对应,其含义分别为:紧急标志、有意义的应答标志、推、重置链接标志、同步序列号标志、完成数据发送标志

窗口大小

目的主机使用该域告知源主机,期望收到的TCP数据段的大小,用于TCP报文的流量控制

校验和

验证收到的数据是否正确

紧急指针

该域只有在URG标志位被置位后才有效,用来指向段内的最后一个字节位置

选项

至少一个字节的可变长域标识哪个选项

数据

最大为MSS,MSS可以在源主机和目的主机之间协商

填充

保证空间的可预测性、定时和规范的大小。该域加入额外的零以保证TCP包头是32的整数倍

3.2  UDP包头的结构

0                                    15 16                                 31

16位源端口号

16位目的端口号

用户数据报的总长度

校验和

数据

图2-3  UDP包头结构

UDP包头的各个字段的含义如下表2-2所示。

表2-2  IP包头的含义

名称

作用

源端口

记录源主机的端口号,源端口和源IP地址的作用是标识报的返回地址

目的端口

记录目的主机的端口号,用来指明接受该报文的计算机上的应用程序的地址接口

总长度

UDP数据包的总长度

校验和

验证收到的数据是否正确

 

3.3  IP包头的结构

       0                                  15 16                               31

4位

版本号

4位首部长度

8位服务器类型

16位总长度(字节数)

16位标识

3位标识

13位片偏移

8位生存时间(TTL)

8位协议

16位首部检验和

32位源IP地址

32位目的IP地址

选项(如果有)

数据

图2-4  IP包头结构

IP包头的各个字段的含义如下表2-3所示。

表2-3  IP包头的含义

名称

作用

版本

IP包头中前4位标识了IP的操作版本,即IPv4或者IPv6

首部长度

用来指明IP包头的长度,以32位(4字节)为单位表示

服务类型

 

总长度

IP包最大长度为65535字节

16位标识

每个IP报文被赋予唯一的16位标识,用于标识数据报的分段

分段标志

(3位标识)包括3个1位标志,标识报文是否允许被分段和是否使用了这些域

分段偏移

指出分段报文相对于整个报文开始处的偏移,该值以64位(8字节)为单位递增

生存时间

IP报文不允许在广域网中永久漫游。必须将其限制在一定的TTL内

协议

8位域指示IP包头之后(即运输层)所使用的协议,如VINES、TCP、UDP等

校验和

验证收到的数据是否正确

源IP地址

指明源计算机的IP地址

目的IP地址

指明目的计算机的IP地址

填充

为了保证IP包头的长度是32的整数倍,要填充额外的零

4、基于TCP的通信连接

对比上述给出的TCP和UDP的包头格式,可将UDP看成是TCP包头的简化版,所以搞清楚TCP连接建立的过程,UDP的建立过程也不难理解。以下介绍基于TCP通信的建立过程。

4.1  通信前期—3次握手建立TCP连接

TCP通信连接的建立需要经过三次握手,以此建立一个可靠的信息交付通路。为了更加清楚的解释三次握手的过程以及其意义,首先对TCP包头的6个标志位做进一步的解释。

URG标志位:此标志表示TCP包的紧急指针域是有效的(即16位紧急指针域的使能开关)。用来保证TCP连接不被中断,并且督促中间层设备(例如路由器)要尽快处理这些数据。

ACK标志位:此标志表示TCP包的应答域是有效的。就是说前面所说的TCP确认序列号将会包含在TCP数据包中。该位取值1表示应答域数据有效,0表示应答域数据无效(即32位确认序列域的使能开关)。

PSH标志位:这个标志位表示Push操作。所谓Push操作就是指在数据包到达接收端以后,立即传送给应用程序,而不是在缓冲区中排队。

RST标志位:这个标志表示连接复位请求。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包。

SYN标志位:此标志表示TCP包的同步序号域是有效的(即32位序列域的使能开关)。SYN标志位和ACK标志位搭配使用,用来请求建立连接。

FIN标志位:表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的TCP数据包后,连接将被断开。

6个标志位各司其职,不同的组合实现不同的功能,但也并非所有组合均是合法的。不合法的标志位组合如下:

(1) SYN和FIN同时被置1                         (2) FIN位被置1,但ACK位没有被置1

(3) SYN和RST同时被置1                       (4) PSH位被置1,但ACK位没有被置1

(5) FIN和RST同时被置1                         (6) URG位被置1,但ACK位没有被置1

(7) URG和PSH同时被置1                      (8) 所有标志位都为0

TCP连接建立时,每个步骤及作用如下所述:

(1)当客户机请求与服务器进行通信时,会首先发送一个连接请求报文段。该报文的首部中,同步位SYN=1,同时随机选择一个初始化序列号seq=x,并指明要连接的服务器上的端口号。TCP规定,SYN报文段(即SYN=1的报文段)不能携带数据,但是仍然要消耗掉一个序号。之后,TCP客户端进程进入SYN_SENT(同步已发送)状态。

(2)服务器收到客户机的连接请求,如果同意该连接请求,则向客户机发送确认报文段,建立连接。在确认报文段应把SYN和ACK标志位都置1,确认序列号是ack=x+1,同时服务器也为自己选择一个初试序列号seq=y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。这时,TCP服务器端进程进入STN_RCVD(同步收到)状态。

(3)TCP客户端进程收到服务器的确认信息之后,还要向服务器给出确认(即对服务器确认信息的确认)。确认报文段的ACK置1,确认号ack=y+1,而自己的序号seq=x+1。TCP标准规定,ACK报文段(即ACK=1的报文段)可以携带数据。但是如果不携带数据则不消耗序号,在这种情况下,下一个数据报文段的序号仍是seq=x+1。这时,TCP连接已经建立,客户机进入ESTABLISHED(已建立连接)状态。当服务器收到客户机的确认信息之后,也进入ESTABLISHED状态。

经过上述3步,客户机就与服务器成功建立起一个TCP连接。上述就是经典的3次握手过程,TCP连接建立时3次握手过程如图2-5所示。

图2-5   3次握手建立TCP连接

    三次握手的简单总结:

第一次握手:客户机发送同步报文段(SYN=1),并随机产生seq x=12345676的数据包到服务器,服务器由SYN=1知道,客户机要求建立联机;

第二次握手:服务器收到请求后要确认联机信息,向客户机发送应答序列号ack=x+1=12345677,SYN=1,ACK=1,并随机产生服务器的序列号seq y=7654321的包;

第三次握手:客户机收到应答信息并检查ack的值是否等于x+1(即12345677),以及位码ack是否为1。若正确,客户机会再发送ack=y+1=7654322,ACK=1的报文段。服务器收到后确认ack(即7654322)的值与ACK=1,则连接建立成功,完成三次握手。

SYN报文段不携带数据,但消耗序列号。ACK报文段用来携带数据,消耗序列号,且数据发送期间ACK恒为1。

4.2  通信中期—数据的收发

关于TCP包头中32位序列号的说明:占用4个字节。范围是[0,232-1],共232(即4284967296)个序列号。序号增加到232-1后,下一个序号就回到0.也就是说,序号使用mod232运算。TCP是面向字节流的。在一个TCP连接中传输的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始号必须在建立连接时设定。TCP包头中的32位序号字段值则指的是本报文段所发送的数据的第一个字节的序号。例如,本报文段的32位序号字段值是301,而携带的数据共有100字节。这就表明:本报文的有效数据的第一个字节的序号是301,而最后一个字节的序号是400。显然,下一个报文段(如果还有的话)的数据序号应当从401开始,即下一个报文段的32位序号字段值应为401。该字段与数据发送有密切的关系。

例如,PC1向PC2发送数据,该数据为5000字节。该数据到达传输层,使用TCP传输协议,给每个字节加一个序列号,序列号是从[0,232-1]之间随机产生的。比如该报文的第一个字节的序列号为x,第二个字节的序列号就是x+1,最后一个字节的编号就是x+4999。传输层在传送数据时如果数据比较大会进行分段传送,假设每100个字节分一个报文片段。那么第一个报文片段的第一个字节序列号肯定是x,该报文片段最后一个字节的序列号就是x+99,那么就用x来表示整个报文片段。第二个报文片段的序列号范围是x+100—x+199,用X+100代表第二个报文片段。第三个x+200—x+299……以此类推。5000字节的数据,最终被分成50个报文片段发送。

如果接受方正确的接收了第一个报文段就会发送一个确认信息。确认的目的就是表示准确的收到了一个报文,并通知发送方希望接受的下一个报文段的序号是什么。例如,如果接收方收到了第一个报文,该报文的有效数据的序号是x到x+99,如果接受方还想继续接受第二个报文就需要向发送方发送确认号x+100,表示我准确的收到了x+99这段数据,我希望你从x+100这个序号继续发送。

数据收发过程的简单总结:

接收到发送方发送的数据后,验证正确性,并告知发送方希望收到的下一个报文分组的首字节的序列号是多少。

4.3  通信后期—4次挥手关闭TCP连接

由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭。关闭原则是当一方完成数据发送任务后,发送一个FIN报文段(FIN=1的报文段)来终止这一方向的连接。收到一个FIN报文段只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到另外一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

图2-6   4次挥手关闭TCP连接

数据传输结束后,通信双方都可以释放链接。如图2-6所示,客户机和服务器都处于ESTABLISHED状态。客户机的应用程序先向服务器发出TCP连接释放报文段,并停止再发送数据,主动关闭TCP连接。客户机把连接报文段首部的FIN置1,其序列号seq=u(u的值等于客户机之前已经发送的有效数据所消耗的的最后一个字节序号加1)。这时客户机进入FIN-WAIT1(终止等待1)状态,等待服务器的确认。注意,TCP规定,FIN报文段即使不携带数据,仍然消耗掉一个序号。

服务器收到连接释放报文段后即发出确认报文段,确认号是ack=u+1,而这个报文段(应答报文段)自己的序号是v(v的值等于服务器之前已经发送的有效数据所消耗的的最后一个字节序号加1)。之后B就进入CLOSE-WAIT(关闭等待) 状态。此时,TCP服务器进程会通知高层应用进程。因此,从客户机到服务器方向的连接就被释放了,这时的TCP连接处于半关闭状态,即客户机已经没有数据要发送了,但是服务器如果发送数据的话,客户机仍然要接收。也就是说,从服务器到客户机方向的连接并未关闭。该状态可能会持续一段时间。

客户机收到服务器的确认报文段后,就进入FIN-WAIT-2(终止等待2)状态,等待服务器发出连接释放请求报文段。若服务器已经没有需要向客户机发送的数据,其应用进程就会通知TCP释放连接。这时服务器发出的连接释放报文段必须使FIN=1。假定服务器的序号为w(半关闭状态,服务器有可能又发送了一些数据)。服务器必须重复上次已经发送过的确认号ack=u+1。这时服务器就进入LAST-ACK(最后确认)状态,等待客户机的确认。

客户机收到服务器的连接释放报文段后,必须对此发出确认。在确认报文中把ACK置1,确认号ack=w+1,而自己的序列号是seq=u+1(TCP标准规定,之前发送的FIN报文段要消耗一个序号)。之后客户机进入到TIME-WAIT(时间等待)状态。注意,此时服务器到客户机方向的TCP还没有释放掉。必须经过时间等待计时器设置的2MSL后,客户机才进入到CLOSED状态。时间MSL叫做最长报文寿命。早期规定MSL=2分钟,因此客户机进入TIME-WAIT状态后要经过2MSL=4分钟,才能进入到CLOSED状态,才能开始建立下一个新的连接。至此客户机和服务器双向的TCP连接断开。

关于2MSL时间的一些补充说明:

为什么客户机在TIME-WAIT状态必须等待2MSL的时间,理由有两个。

第一、为了保证客户机发送的最后一个ACK报文段能够到达服务器。这个ACK报文段是有可能丢失的,因而使得处在LAST-ACK状态的服务器收不到对已经发送的FIN+ACK报文段的确认。服务器会因为超时没有接收到对FIN+ACK报文确认报文而重新发送FIN+ACK报文段,而客户机就可以在2MSL时间内收到这个重传的FIN+ACK报文段。接着客户机会重传一次确认报文,重新启动2MSL计时器。最后,客户机和服务器都进入到CLOSED状态。如果客户机在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段就立即释放连接,那么如果ACK意外丢失,服务器等待超时后,重传的FIN+ACK报文段就无法接收,因而也不会再重传ACK的确认报文段。这样服务器就无法按照正常步骤进入CLOSED状态。

第二、防止已经失效的连接请求报文段出现在新的连接中。客户机在发送完最后一个ACK报文后,再经过2MSL时间,就可以使本连接持续时间内所产生的所有报文都从网络中消失(MSL远远大于TTL)。这样就可以使下一个新的连接中不会出现旧连接中的请求报文段。服务器只要收到了客户机发来的确认报文段,就会进入CLOSED状态。

三、Linux下socket套接字编程

1、TCP编程模型

1.1 套接字socket简介

Linux中的网络编程通过socket接口实现。socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”。所以说,所谓socket,既是一种系统调用,也是一种特殊的I/O文件。可以用【打开open】–>【读写write/read】->【关闭close】模式来操作。socket在物理上对应一个网络连接,在编程上对应一个文件描述符。当应用程序调用write向socket这种文件描述符写入数据时,数据就会被发送至网络,进而到达对方的机器。

一个完整的socket都有一个相关描述【协议、本地地址、本地端口、远程地址、远程端口】,也就是说一个完整的socket是一个5元组。每个socket对应本地唯一的由操作系统分配的socket号(文件描述符)。

1.2 套接字socket的分类

(1) 流式套接字(SCOK_STREAM)。流式套接字可以提供可靠的、面向连接的通信流。它使用TCP协议。TCP协议保证了数据传输的正确性和有序性。

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

(3) 原始套接字。原始套接字允许对底层协议,如IP或ICMP直接访问,主要用于新的网络协议实现的测试。

1.3 TCP套接字socket编程步骤

TCP网络编程整体步骤如图3-1所示。

 

图3-1  TCP socket编程模型

(1) 服务器端(server)编程步骤

① 调用socket创建一个不与任何网络连接相对应的套接口(socket),其返回值是一个文件描述符;

② 调用bind,将刚创建的socket与本机的IP地址和端口号绑定,此时该socket为一个3元组;

(IPv4协议,本地IP,本地端口)

       ③调用listen,将socket设定为被动套接口(用于监听),专门用于监听客户端的网络连接请求;

       ④调用accept,使socket进入到可以接受网络连接请求的状态。此时服务器将进入阻塞态,直到有客户端的连接到达,accept将返回另一个socket(文件描述符),这个socket被称为连接套接口(用于通信),此套接口是一个5元组(其中的远程IP和远程端口来自于客户机发起连接请求时,发送到服务器的数据包,其实就是3次握手时第一个数据包);

⑤ 调用文件读取函数(如read,recv)从连接套接口接收客户端发送过来的数据。如果此时客户端并未发送数据,则read将会阻塞到有数据到达为止;

⑥ 对客户端的数据进行处理,之后调用write函数向连接套接口写入处理后的数据,该数据将通过网络发送至客户端;

⑦ 当所有的数据都已经处理完毕,将调用close函数关闭连接套接口,这将在网络上激发4次挥手终止序列,在客户机的配合下,成功关闭网络连接,结束通信。

(2) 客户机端(client)编程步骤

① 调用socket创建一个不与任何网络连接相对应的套接口(socket),其返回值是一个文件描述符;

② 调用connect,将服务器IP和端口号作为参数传入,OS将选择一个本机尚未使用的大于1024的端口号作为本地端口号(此时,客户端的socket已经是5元组),主动发起连接请求,经过3次握手,connect将返回,此时socket已经对应上物理上的一个网络连接通道;

③ 调用write向socket写入数据,该数据将到达服务器,被read函数所接收;

④ 调用read函数,等待服务器经socket回送的处理数据;

⑤ 当所有数据接收完毕,调用close函数,激发4次挥手终止序列,关闭网络连接,结束通信。

1.4 套接字编程基础知识

(1) 套接口地址结构体

bind()函数用来将新创建的网络套接字(设备描述符)和实际网络实体进行绑定。所谓绑定就是建立套接字设备描述符和物理网络实体信息之间的映射。对套接字设备描述符进行操作,就相当于对物理网络实体操作。而物理网络实体的信息就是用套接口地址结构体来存放的。套接口地址结构体如下:

typedef       unsigned long          uint32_t;

typedef       uint32_t                      in_addr_t;

struct    in_addr

{      

in_addr_t             s_addr;         //存放IP地址

};

 

typedef       unsigned short int           sa_family_t;

typedef       unsigned int                     uint16_t;

typedef       uint16_t                              in_port_t;

struct    sockaddr_in                                            //16字节,存放物理网络实体的信息(Linux套接字结构体)

{     

 sa_family_t                 sin_family;                 //2字节,指定IP协议版本(IPv4 IPv6)

 in_port_t                     sin_port;                    //2字节,指定端口号

         struct  in_addr           sin_addr;                   //4字节,指定IP地址

         unsigned char          sin_zero[  sizeof(structsockaddr)-

   (sizeof(unsignedshortint))-

    sizeof(in_port_t)-

    sizeof(structin_addr)  ];

};

     *******************************************************     分隔线      ********************************************************

typedef       unsigned short int   sa_family_t;         …………………………//通用套接字接口结构体

struct    sockaddr

{      

sa_family_t         sa_family;

        char                    sa_data[14];

};

 

struct    sockaddr_un                        …………………………………………//UNIX套接字接口结构体

{      

sa_family_t        sun_family;

        char                   sun_path[108];

};


① 分隔线以下的套接字结构体中,sockaddr_un套接字结构体为UNIX所使用。分隔线以上的sockaddr_in套接字结构体是Linux网络编程中常用的socket套接口结构体。bind、connect等函数,可以根据套接口类型的不同,处理不同的address family(地址族、协议族),例如AF_UNIX、AF_INET等。但不同的地址族有不同套接口地址结构,例如sockaddr_un和sockaddr_in,因此需要一个统一的通用套接口地址结构体,例如sockaddr套接字结构体作为bind、connect函数的参数。这样,内核就可以通过检查sa_family的值确定套接口地址结构体的类型,然后对套接口地址结构体指针进行正确的类型转换。

② 要解决①中提出的问题,对于ANSI标准来说很容易,只要定义void *即可。但是socket编程早于ANSI,所以其采用的解决方法是,依然定义通用套接字接口地址结构体sockaddr(事实上,这样做不伦不类,因为sockaddr的长度与sockaddr_in相同,为16字节,但sockaddr_un却是110字节),同时这也导致了编程中调用bind等函数时,必须对第二参数(指针)进行强制类型转换,不然就会报错。

③ sin_addr (s_addr)用来存放IP地址,sin_port用来存放端口号,sin_family用来存放使用的IP协议版本;

④ struct  sockaddr_in长度为16字节,其中sin_zero长度为8字节,暂时未用,应全部初始化为0;

⑤ 可以通过两种方式访问32位IP地址:1)访问结构体(sin_addr);2)访问32位无符号整数(sin_addr.s_addr)。由于历史原因,sin_addr 是一个结构体,且其成员为一个union共用体,以便方便的访问A、B、C类的网络地址,但是随着无类地址的广泛使用,该union被废除,sin_addr结构体的成员变成32位整数s_addr。

(2) 字节序列转换

数据在内存中的存放顺序有两种,即大端序和小端序。采用大端序的系统称为大端系统,采用小端序的系统称为小端系统。英特尔的CPU就是典型的小端系统。对于数据来说,都有高位数据和低位数据,所以大小端定义如下:

所谓大端序是指,数据在内存中的存放规则为,低位数据存放在内存的高地址位,高位数据存放在内存的低地址位。

所谓小端序是指,数据在内存中的存放规则为,低位数据存放在内存的低地址位,高位数据存放在内存的高地址位。

一个简单的记法:大端交叉相对,小端一一相对

由于客户机采用的系统有可能是大端系统,也有可能是小端系统。如果一个大端(小端)系统不加处理地将数据经由网络发送给一个小端(大端)系统,则会出现错误。例如,如果发送(接收)机是小(大)端系统,则发送short型整数0x3132(先发0x32,后发0x31)到达接收机后,0x32被存放在内存低端,0x31被存放在内存高端,最终将被解释为0x3231,而不是0x3132。这样是有问题的。

所以,在网络数据交换中,要求网络中传输的数据格式必须是网络字节序。网络字节序采用的是大端字节序。所以数据在被发送到网络上之前,需要进行数据格式的转换。当数据接到达接收机之后,接收机会按照自身系统所采用的字节顺序对接收到的数据进行相应的格式转换,以保证通信数据的正确性。

常用的字节序列转换函数如下:

#include <arpa/inet.h>                                                        //需要包含的头文件

unsigned long int htonl(unsigned long int hostlong)    //将unsigned long int数据的主机字节序转换为网络字节序

unsigned short int  htons(unsignedshort int  hostshort)    //将unsigned short int数据的主机字节序转换为网络字节序

unsigned long int ntohl(unsigned long int netlong)      //将unsigned long int数据的网络字节序转换为主机字节序

unsigned short int ntohs(unsigned short int netshort)      //将unsigned short int数据的网络字节序转换为主机字节序

(3) 地址格式转换

IP地址在计算机中(无论是客户机还是服务器)使用点分十进制的字符串形式表示的,例如192.168.1.103。但是IP协议在进行封包操作时,会将点分格式的地址字符串转换为网络字节序的整型数。而计算机收到数据包之后会进行逆操作,即将网络字节序整型转换为点分格式的IP地址。常用的地址格式转换函数:

#include <sys/types.h>

# include <sys/socket.h>

# include <arpa/inet.h>

int  inet_pton(int  af, const char *  src,  void * det)

函数功能:将点分格式的地址字符串转换为网络字节序的整型数。

返 回值:成功返回1,错误返回-1

函数参数:

-af   采用的协议类型。AF_INET(IPv4)或AF_INET6(IPv6)

-src  点分格式的IP字符串地址

-dst  转换后的整型变量的地址

192.168.6.100 ================>  0x6406a8c0

 

const char *inet_ntop(int  af,  const void *src,  char  *dst, socklen_t  cnt)

函数功能:将网络字节序的整型数转换为点分格式的地址字符串。

返 回值:成功返回转换后字符串的地址,错误返回NULL

函数参数:

-af   采用的协议类型。AF_INET(IPv4)或AF_INET6(IPv6)

-src  网络字节序的整型变量的地址

-dst  转换后的点分格式的IP字符串地址

-cnt  字符串存储空间的大小

0x6406a8c0 ================>  192.168.6.100

(4) IP和域名的转换

一台主机或服务器的IP地址通常很难记住,为了便于记忆,采用域名来标志网络上的一台主机。例如百度服务器的IP地址是202.108.22.5,但是很有有人能记住,通常只要输入域名“www.baidu.com”就可以。域名和IP地址的转换由如下函数实现:

struct hostent  * gethostbyname(const char *hostname)

struct hostent  * gethostbyaddr(const void *addr,int len,inttype)

在<netdb.h>中定义了structhostent结构体,该结构体实现了主机的域名和IP地址的映射:

struct hostent

{

       char  *h_name;                 //主机的正式名称

       char  **h_aliases;                 //主机的别名

       int   h_addrtype;                     //主机的地址类型AF_INET

       int    h_length;                //主机的地址长度        IPv4  32位4字节 /  IPv6  128位16字节

       char  **h_addr_list;              //主机的IP地址列表

}

gethostbyname函数可以将域名(如www.baidu.com)转换为一个结构体指针,在这个结构体中存储了域名和IP地址的对应信息。

gethostbyaddr函数可以将一个socket地址结构体中的IP地址转换为一个结构体指针,在这个结构体中存储了域名和IP地址的对应信息。

上述两个函数,调用失败返回NULL,并设置h_errno错误变量。

 

 

1.5 TCP socket编程实例

(1) 主要API函数

socket

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

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

【函数功能】

socket函数用来创建一个用于网络通信的套接字,并返回该套接字的文件描述符(整型)。

【函数参数】

-   domain指定通信所使用的协议族类型(如AF_INET, AF_INET6,AF_UNIX等。TCP/IP使用的协议类型为PF_INET或AF_INET) 。

-   type指定套接字的类型(TCP套接字为SOCK_STREAM,UDP套接字为SOCK_DGRAM)。

-   protocol指定所使用的协议号。通常该参数的值设为0即可,表示只使用协议族中的其中一种协议。

【返 回 值】

成功,返回新建套接字的文件描述符,失败,返回-1。

==================================================================

bind

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

int bind(int   sockfd,  const struct sockaddr  *addr,   socklen_t  addrlen);

【函数功能】

bind函数用来为新创建的套接字绑定本地IP地址和协议端口号(IP和协议端口存放在套接字接口结构体中)。

【函数参数】

-   sockfd指定要进行绑定操作的套接字的文件描述符。

-   addr指定套接字接口结构体的地址,该结构体中存放了IP地址和端口号。

-   addrlen指定套接字接口结构体字节的大小。

【返 回 值】

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

==================================================================

listen

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

int listen(int sockfd,  int  queuelen);

【函数功能】

listen函数将服务器端socket接口设置为被动状态(即监听套接字),用于监听客户端的连接请求。

【函数参数】

-   sockfd指定要进行监听操作的套接字的文件描述符。

-   queuelen指定连接请求队列的大小(应对多请求任务)。

【返 回 值】

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

==================================================================

accept

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

int accept(int  sockfd,      struct sockaddr  *addr, socklen_t  *addrlen);

【函数功能】

accept函数用于从连接请求队列中取走一个连接请求(或请求队列为空时,以阻塞模式等待,知道新的连接请求到达),并为该请求创建一个新的用于通信的套接字,并返回通信套接字的文件描述符。accept只能用于流式套接字。

【函数参数】

-   sockfd指定监听套接字的文件描述符。

-   addr套接字地址结构体指针。调用accept成功后,在该结构体中填入远程机器的IP地址和协议端口号。

-   addrlen初始值设定为struct sockaddr结构体大小的存放地址,调用accept成功后,在其中填入远程机器socket地址的实际大小。

【返 回 值】

成功,返回通信套接字的文件描述符(非负),失败,返回-1。

==================================================================

connect

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

int connect(int  sockfd,   conststructsockaddr  *addr,socklen_t  addrlen);

【函数功能】

connect函数允许调用者为先前创建的套接字指明远程终端的地址。如果套接字使用TCP,connect就是用3次握手创建一个连接。

【函数参数】

-   sockfd指定要进行连接的远程终端的套接字文件描述符。

-   addr指定远程终端的IP地址和协议端口号。

-   addrlen指定struct sockaddr结构体的大小。

【返 回 值】

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

==================================================================

send

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

ssize_t  send(int  sockfd ,constvoid  *buf , size_t  len ,  int  flags);

【函数功能】

send函数用来发送消息到与之建立网络连接的套接字。

【函数参数】

-   sockfd指定发送端的套接字文件描述符。

-   buf指定待发送数据(在本地)的地址。

-   len指定待发送数据的最大字节。

-   flags指定一些额外的功能,功能标志位,该位通常为0,此时等价于write函数。

【返 回 值】

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t是无符号整型。

成功,返回实际发送的数据字节个数,失败,返回-1,并将errno变量设置为相应的值。

==================================================================

write

【函数概要】

#include <unistd.h>

ssize_t  write(int fd ,  const       void  *buf  ,  size_t       count);

【函数功能】

write函数用来向已经打开的文件(即获得该文件的描述符)写入数据。

【函数参数】

-   fd指定要写入数据的文件描述符。

-   buf指定待写入数据(在本地)内存中的地址。

-   count指定期望写入的数据的最大字节。

【返 回 值】

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t是无符号整型。

成功,返回实际写入的字节个数,失败,返回-1。

==================================================================

recv

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

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

【函数功能】

recv函数用来接收远程主机通过已经建立的网络连接套接字所发送来的消息。

【函数参数】

-   sockfd指定接收端的套接字文件描述符。

-   buf指定被接收的数据在本地内存中的地址。

-   len指定待接收数据的最大字节。

-   flags指定一些额外的功能,功能标志位,该位通常为0,此时等价于read函数。

【返 回 值】

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t是无符号整型。

成功,返回实际接收到的数据字节个数。失败,返回-1。

==================================================================

read

【函数概要】

#include <unistd.h>

ssize_t  read(int fd ,  void  *buf  ,  size_t count);

【函数功能】

read函数用来从已经打开的文件(即获得该文件的描述符)中读取数据。

【函数参数】

-   fd指定要读取的文件的文件描述符。

-   buf指定读取到的数据在本地内存中的存放地址。

-   count指定期望读取到的数据的最大字节。

【返 回 值】

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t是无符号整型。

成功,返回实际读取到的字节个数,若已经到文件末尾返回0。失败,返回-1。

==================================================================

close

【函数概要】

#include <unistd.h>

int close(int  fd);

【函数功能】

close函数用来关闭一个打开的文件。

【函数参数】

-   fd指定要关闭的文件的文件描述符。

【返 回 值】

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

 

(2)TCP程序实例分析

服务端程序server_iter.c

#include <stdlib.h>

#include <ctype.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/socket.h>

#include <sys/un.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <netdb.h>

 

char      host_name[20];        //存放客户机的主机名

int         port= 8000;                //指定服务器端进程所使用的端口号

 

int main(void)

{

       structsockaddr_in     sin, pin;

       int sock_descriptor, temp_sock_descriptor,address_size;

       int i, len, on=1;

       charbuf[16384];

    /***********************创建套接字***********************/

       sock_descriptor= socket(AF_INET, SOCK_STREAM, 0);

       if(sock_descriptor== -1)

{      perror("call to socket");      exit(1);   }

       //setsockopt(sock_descriptor,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));

       /***********************填充地址结构体***********************/

bzero(&sin,sizeof(sin));

       sin.sin_family= AF_INET;

       sin.sin_addr.s_addr= INADDR_ANY;

       sin.sin_port= htons(port);

       /***********************套接字和地址结构体进行绑定***********************/

if (bind(sock_descriptor,(struct sockaddr*)&sin, sizeof(sin)) == -1)

      {      perror("call to bind");        exit(1);   }

/***********************将套接字设置为监听状态***********************/

       if(listen(sock_descriptor,100) == -1)

{      perror("call to listen");        exit(1);   }

       printf("Acceptingconnections ...\n");             //打印提示信息

/***********************进行数据交互***********************/

       while(1)

{

              address_size= sizeof(pin);

/***********************处理连接请求,创建通信套接字***********************/

              temp_sock_descriptor= accept(sock_descriptor, (struct sockaddr *)&pin, &address_size);

              if(temp_sock_descriptor== -1)

{      perror("call to accept");      exit(1);   }

/***********************借助通信套接字进行数据的接收***********************/

              if(recv(temp_sock_descriptor,buf, 16384, 0) == -1)

{      perror("call to recv");         exit(1);   }

              inet_ntop(AF_INET,&pin.sin_addr, host_name, sizeof(host_name)); //将网络字节序转换为点分格式

              printf("receivedfrom client(%s): %s\n", host_name, buf);        //打印接受到的信息

    /* for this server example, we just convert thecharacters to upper case: */

              len= strlen(buf);

              for(i= 0; i < len; i++)

                     buf[i]= toupper(buf[i]);

    /***********************借助通信套接字进行数据的发送***********************/

              if(send(temp_sock_descriptor,buf, len+1, 0) == -1)

{      perror("call to send");        exit(1);   }

    /***********************关闭通信套接字***********************/

close(temp_sock_descriptor);

              //sleep(60);

       }

/***********************关闭监听套接字***********************/

       close(sock_descriptor);

}

 

客户端程序client.c

#include <stdlib.h>

#include <ctype.h>

#include <unistd.h>

#include <string.h>

#include <stdio.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <netdb.h>

 

char *host_name ="127.0.0.1";             //指定服务器IP地址,回环测试,使用同一张网卡进行数据的发送和接收

int port = 8000;                                //指定服务器端口号

 

int main(int argc, char *argv[])

{

       charbuf[8192]; 

       intsocket_descriptor;

       structsockaddr_in pin;

       char*str = "A default test string";

       /***********************通过启动参数设置服务器IP和测试字符串***********************/

       if(argc< 2)

{

              printf("Usage:client \"Any test string\" \"ip address\"\n");

              printf("Wewill send a default test string.\n");

       }

else

{

              str= argv[1];

              if(argc ==3)

              host_name= argv[2];

       }

       /***********************填充地址结构体***********************/

       bzero(&pin,sizeof(pin));

       pin.sin_family= AF_INET;

       inet_pton(AF_INET,host_name, &pin.sin_addr);

       pin.sin_port= htons(port);

       /***********************创建套接字***********************/

if((socket_descriptor =socket(AF_INET, SOCK_STREAM, 0)) == -1)

{      perror("Error opening socket\n");    exit(1);   }

       /***********************连接服务器***********************/

if(connect(socket_descriptor, (struct sockaddr *)&pin, sizeof(pin)) == -1)

{      perror("Error connecting to socket\n");   exit(1);   }

      

       printf("Sendingmessage %s to server...\n", str);  //打印提示信息

/*     if (send(socket_descriptor, str,strlen(str)+1, 0) == -1)

{      perror("Error in send\n");   exit(1);   }      */

       /***********************向服务器发送数据***********************/

if(write(socket_descriptor, str, strlen(str)+1) == -1)

{      perror("Error in send\n");   exit(1);   }

       printf("..sent message.. wait for response...\n");

       //close(socket_descriptor);

       //exit(1);

/*     if(recv(socket_descriptor, buf, 8192, 0) ==-1)

{      perror("Error in receiving responsefrom server\n");   exit(1);   }      */

       /***********************读取服务器回传的数据***********************/

if(read(socket_descriptor,buf, 8192) == -1)

{      perror("Error in receiving response from server\n");   exit(1);   }

       printf("\nResponsefrom server:\n\n%s\n", buf);

/***********************关闭通信套接字***********************/    

close(socket_descriptor);

       return1;

}

2、UDP编程模型

2.1 UDP套接字socket编程步骤

UDP网络编程整体步骤如图3-2所示。

(1)服务器端(server)编程步骤

① 调用socket创建一个不与任何网络连接相对应的套接口(socket),其返回值是一个文件描述符;

② 调用bind,将刚创建的socket与本机的IP地址和端口号绑定,此时该socket为一个3元组;

(IPv4协议,本地IP,本地端口)

     ③调用recvfrom,从套接口接收客户端发送过来的数据(同时获取客户端的IP地址和端口号),如果此时客户端并未发送过来数据,那么recvfrom将阻塞到数据到达为止;

       ④调用sendto(将客户端的IP地址和端口号作为参数传入),向套接口写入处理后的数据,该数据将通过网络到达客户端;

⑤当所有的数据都已经处理完毕,将调用close函数关闭连接套接口,关闭网络连接,结束通信。

 (2)客户机端(client)编程步骤

① 调用socket创建一个不与任何网络连接相对应的套接口(socket),其返回值是一个文件描述符;

② 调用sendto(将服务器IP和端口号作为参数传入)向socket中写入数据, OS将选择一个本机尚未使用的大于1024的端口号作为本地端口号,并选择本机的IP地址作为本地IP(此时,客户端的socket已经是5元组)该数据到达服务器后,被recvfrom函数读取;

③ 调用recvfrom函数,等待从socket读取服务器通过sendto回传的数据;

④ 当所有数据接收完毕,调用close函数,关闭网络连接,结束通信。

图3-2  UDP socket编程模型

 

2.2 UDP socket编程实例

 (1)主要API函数

sendto

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

ssize_t  sendto(int  sockfd,  const void * buf ,  size_t  len ,  int  flags ,  const struct sockaddr *dest_addr,

socklen_t addrlen);

【函数功能】

sendto函数用来发送消息到与之建立网络连接的套接字(最后两个参数为NULL和0时,等价于send函数)。

【函数参数】

-   sockfd指定发送端的套接字文件描述符。

-   buf指定待发送数据(在本地内存)的地址。

-   len指定待发送数据的最大字节。

-   flags指定一些额外的功能,功能标志位,该位通常为0。

-   dest_addr指定数据接收端的地址结构体(含IP地址和端口号)的指针。

-   addrlen数据接收端地址结构体的大小。

【返 回 值】

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t是无符号整型。

成功,返回实际发送的数据字节个数,失败,返回-1。

==================================================================

recvfrom

【函数概要】

#include <sys/types.h>

#include <sys/socket.h>

ssize_t  recvfrom(int  sockfd ,  void  *buf ,  size_t  len ,  int flags ,  struct sockaddr  *src_addr ,

                           socklen_t  *addrlen);

【函数功能】

recvfrom函数用来接收远程主机通过已经建立的网络连接套接字所发送来的消息。

【函数参数】

-   sockfd指定接收端的套接字文件描述符。

-   buf指定被接收的数据在本地内存中的地址。

-   len指定待接收数据的最大字节(即缓冲区的最大长度)。

-   flags指定一些额外的功能,功能标志位,该位通常为0。

-   src_addr指向的结构体将被对端(即数据发送端)地址(含IP和端口号)所填充。

-   addrlen所指向的位置,调用前应填入src_addr指向的结构体的大小,调用后将被填入对端地址的实际大小。

注:若不需要对端的IP地址和端口号,将src_addr和addrlen设置为NULL即可。

【返 回 值】

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t是无符号整型。

成功,返回实际接收到的数据字节个数。失败,返回-1。

(2)UDP程序实例分析

服务端程序udpserver.c

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <stdio.h>

#include <errno.h>

#include <stdlib.h>

#include <strings.h>

#include <unistd.h>

#include <string.h>

#include <ctype.h>

 

#define SERVER_PORT 8888                     //定义服务器端口号

#define MAX_MSG_SIZE 1024                    //定义缓冲区大小

 

void udps_respon(int sockfd)

{

       structsockaddr_in     addr;

       intn, i;

       unsignedint        addrlen = sizeof(structsockaddr_in);

       charmsg[MAX_MSG_SIZE];

while(1)

      {     /***********************数据的接收***********************/

              n= recvfrom(sockfd, msg, MAX_MSG_SIZE, 0, (struct sockaddr*)&addr,&addrlen);

              msg[n]= 0;

              /***********************显示收到的信息***********************/

              fprintf(stdout,"I have received %s", msg);

              /***********************数据的处理***********************/

              n= strlen(msg);

                for(i = 0; i < n; i++)

                        msg[i] =toupper(msg[i]);

             /***********************数据的发送***********************/

              sendto(sockfd,msg, n, 0, (struct sockaddr *)&addr, addrlen);

       }

}

int main(void)

{

       intsockfd;

       structsockaddr_in addr;

       /***********************创建套接字***********************/

       sockfd= socket(AF_INET, SOCK_DGRAM, 0);

       if(sockfd < 0)

{      perror("socket");   exit(1);      }

/***********************填充地址结构体***********************/

       bzero(&addr,sizeof(struct sockaddr_in));

       addr.sin_family= AF_INET;

       addr.sin_addr.s_addr= htonl(INADDR_ANY);

       addr.sin_port= htons(SERVER_PORT);

/***********************套接字和地址结构体进行绑定***********************/  

if (bind(sockfd, (structsockaddr *)&addr, sizeof(struct sockaddr_in)) < 0)

{     perror("bind");      exit(1); }

       udps_respon(sockfd);      //数据交互

       close(sockfd);                   //关闭通信套接字

return 0;

}

 

客户端程序udpclient.c

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <errno.h>

#include <stdio.h>

#include <unistd.h>

#include <string.h>

#include <stdlib.h>

#include <arpa/inet.h>

 

#define MAX_BUF_SIZE 1024              //定义缓冲区大小

 

void udpc_requ(int sockfd, conststruct sockaddr_in *addr, int len)

{

       charbuffer[MAX_BUF_SIZE];

       intn;

       while(1)

{     /***********************数据的发送***********************/

              fgets(buffer,MAX_BUF_SIZE, stdin);   //从标准输入文件获取数据

              sendto(sockfd,buffer, strlen(buffer), 0, (struct sockaddr *)addr, len);

              printf("Ihave sent to server %s", buffer);            

              printf("Waitingrespond from server\n");

              /***********************数据的接收***********************/

bzero(buffer,MAX_BUF_SIZE);

              n= recvfrom(sockfd, buffer, MAX_BUF_SIZE, 0, NULL, NULL);

              buffer[n]= 0;

              printf("Ihave received from server ");

              fputs(buffer,stdout);                //将回传数据显示到屏幕上(标准输出)

              printf("\n");

       }

}

 

int main(int argc, char **argv)

{

       intsockfd, port;

       structsockaddr_in addr;

       /***********************通过启动参数设置服务器IP和测试字符串***********************/

       if(argc != 3)

{      fprintf(stderr, "Usage:%s server_ip server_port\n",argv[0]);    exit(1);   }

       if((port = atoi(argv[2])) < 0)

{      fprintf(stderr, "Usage:%s server_ip server_port\n",argv[0]);    exit(1);   }

       /***********************创建套接字***********************/

sockfd = socket(AF_INET,SOCK_DGRAM, 0);

       if(sockfd < 0)

{     fprintf(stderr, "Socket Error:%s\n", strerror(errno));     exit(1);   }

       /***********************填充地址结构体***********************/

bzero(&addr,sizeof(struct sockaddr_in));

       addr.sin_family= AF_INET;

       addr.sin_port= htons(port);

       if(inet_aton(argv[1], &addr.sin_addr) < 0)

{     fprintf(stderr, "Ip error:%s\n", strerror(errno));     exit(1);   }

       udpc_requ(sockfd,&addr, sizeof(struct sockaddr_in));             //数据交互

       close(sockfd);             //关闭通信套接字

       return0;

}

 

说明:

对服务器而言:

1、没有调用accept,因为不需要建立连接;

2、一个套接字接口可以接受不同的IP发送来的数据,对端socket地址由recvfrom获得。

对客户机而言:

1、没有调用connect,因为不需要建立连接;

2、首次发送数据时,由操作系统临时选择本地IP地址和端口;由sendto指定目的IP地址和端口号,因此一个套接字接口可以向不同的IP发送数据。

 

 

 

 

 

 

 

 

 

 

 

 

附录

按照posix标准,一般整形对应的*_t类型为:

1字节     uint8_t             2字节    uint16_t           4字节     uint32_t           8字节    uint64_t

附:C99标准中inttypes.h的内容

00001 /*

00002    inttypes.h

00003 

00004    Contributors:

00005      CreatedbyMarek Michalkiewicz <[email protected]>

00006 

00007    THISSOFTWAREIS NOT COPYRIGHTED

00008 

00009    Thissourcecode is offered for use in the public domain.  You may

00010    use,modifyor distribute it freely.

00011 

00012    Thiscodeis distributed in the hope that it will be useful, but

00013    WITHOUTANYWARRANTY.  ALLWARRANTIES, EXPRESS OR IMPLIED ARE HEREBY

00014    DISCLAIMED.  Thisincludes but is not limited towarranties of

00015    MERCHANTABILITYorFITNESS FOR A PARTICULAR PURPOSE.

00016  */

00017 

00018 #ifndef __INTTYPES_H_

00019 #define __INTTYPES_H_

00020 

00021 /* Use [u]intN_t if youneed exactly N bits.

00022    XXX-doesn't handle the -mint8 option.  */

00023 

00024 typedef signed char int8_t;

00025 typedef unsigned char uint8_t;

00026 

00027 typedef int int16_t;

00028 typedef unsigned int uint16_t;

00029 

00030 typedef long int32_t;

00031 typedef unsigned long uint32_t;

00032 

00033 typedef long long int64_t;

00034 typedef unsigned long long uint64_t;

00035 

00036 typedef int16_t intptr_t;

00037 typedef uint16_t uintptr_t;

00038 

00039 #endif

 

 

猜你喜欢

转载自blog.csdn.net/GDUTLYP/article/details/50214673