53.Linux socket编程

学习目标

①熟悉计算机网络体系结构

②掌握socket通信流程

③了解网络编程相关知识

④熟练使用socket编程接口实现网络通信

⑤熟悉socket本地通信

Linux网络编程一般通过socket(套接字)接口实现,本章将以socket编程为主,结合计算机网络基础知识,讲解Linux系统中实现网络编程的方法。

1.1 计算机网络概述

        计算机网络是网络的一种,网络由若干个结点和连接这些结点的链路组成,网络中的结点可以是计算机、交换机、路由器等。

        为了保证通信能顺利进行且进行交互的进程能获取准确、有效的数据信息,进行通信的双方必须遵循一系列事先约定好的规则,这些规则即是网络协议。

1.2协议与体系结构

        网络间通信的过程很复杂,为复杂的过程制定的协议也会非常复杂。因此,人们考虑按照复杂过程中各项工作的性质,将需要实现的工作进行分层并为每一层中的操作制定协议。对网络间的通信过程所划分的层次通常被称为计算机网络的体系结构。

比较常见的体系结构为OSI(Open System Interconnect,开放式系统互联)和TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/互联网协议)。

OSI是Open System Interconnection的缩写,意为开放式系统互联国际标准化组织(ISO)制定了OSI模型,该模型定义了不同计算机互联的标准,是设计和描述计算机网络通信的基本框架。OSI模型把网络通信的工作分为7层,分别是物理层数据链路层网络层传输层会话层、表示层和应用层

这是一种事实上被TCP/IP 4层模型淘汰的协议。在当今世界上没有大规模使用。

TCP/IP(Transmission Control Protocol/Internet Protocol)是指能够在多个不同网络间实现信息传输的协议簇,由网络层的IP协议和传输层TCP协议组成。

其定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。

在OSi诞生时,因特网已实现了全世界的基本覆盖,因此市面上应用最广泛的体系结构为因特网中使用的TCP/IP体系结构;该结构分为四层,分别为应用层、传输层、网际层和网络接口层

计算机网络中通常采用一种包含五层协议的体系结构来讲解各层之间的功能与联系,该体系结构结合了OSI和TCP/IP的优点,分为应用层、传输层、网络层、数据链路层和物理层

以上三种体系结构中各层的对应关系如图10-1所示

                                           图10-1   计算机网络体系结构

五层协议体系结构中各层的功能分别如下:

 1.应用层

        应用层为应用进程提供服务,定义了应用进程间通信和交互的规则。不同的网络应用会采用不同的应用层协议,最常见的有支持万维网应用的HTTP协议、支持电子邮件的SMTP协议等。

2.传输层

        传输层为应用进程提供连接服务,实现连接两端进程的会话。该层定义了两个端到端的协议:TCP协议和UDP协议;这两个协议都使用端口号区分同一台计算机中的不同进程,使用端点为不同计算机中的进程建立连接。端口号在一台计算机中唯一,它是一个16位整数端点由主机地址和端口组成,端点能唯一确定计算机网络中某一台计算机上的某个进程。

(1)TCP协议

TCP数据报格式

         TCP协议即传输控制协议(Transmission Control Protocol),使用该协议的传输层会接收由应用层传输来的、使用8位字节表示的数据流。然后根据协议规则,将数据流分为多个报文段并为每个报文段添加本层的控制信息,生成传递给网络层的数据单元。

        TCP协议是一种面向连接的、可靠的、基于字节流的传输协议。在传递数据之前,双方会先通过一种被称为“三次握手”的协商机制使通信双方建立连接,为数据传输做好准备。为了防止报文

文段丢失,TCP会给每个数据段一个序号,接收端应按序号顺序接收数据。若接收端正常接收到报文段,便会向发送端发送一个确认信息;若发送端在一定时间后未接收到确认信息,便假设报文段已丢失并重新向接收端发送对应报文段。此外,TCP协议还定义了一个校验和函数,用于检测发送和接收的数据,防止产生数据错误。

        通信结束后,通信双方经过“四次握手”关闭连接。由于TCP连接是全双工的,因此每个方向必须单独关闭连接,即连接的一端需要先发送关闭信息到另一端;当关闭信息发送后,发送关闭信息的一端不会再发送信息,但另一端仍向该端发送信息。

(2)UDP协议

        UDP协议即用户数据协议(User Datagram Protocol)。使用UDP协议的传输层中传输的数据是按UDP协议封成的数据报,每个数据报的前8个字节用来存储报头信息,其余字节用来存储需要传输的数据。

        UDP是一种无连接的传输层协议。因为UDP的收发双方并不存在连接关系,按照UDP协议传输数据时,发送方使用套接字文件发送数据报给接收方,之后可立即使用同一个套接字发送给其他数据给另一个接收方;同样地,接收方也可以通过相同的套接字接收由多个发送方发来的数据。

   16位:源端口号        2^16=65536

    16位:目的端口号    

3.网络层

 16位:源端口号        2^16=65536

 16位:目的端口号

 32序号

 32确认序号

 6个标志位

 16窗口大小               

IP地址:可以在网络环境中,唯一标识一台主机

端口号:可以在网络的一台主机上,能唯一标识一个进程。

IP地址+端口号:可以在网络环境当中,唯一标识一个进程。

4.数据链路层

数据链路层简称链路层,该层将从网络层获取的IP数据报组装成帧,在网络结点之间以帧为单位传输数据。基于不同协议的帧有不同的格式。

5.物理层

 1.3  C/S模型:

        client - server

        B/S模型:

        browser - server

                        C/S                                                          B/S 

优点: 缓存大量数据、协议选择灵活                    安全性、跨平台、开发工作量较小                                 速度快、                                                                                   

缺点: 安全性、                                                   不能缓存大量数据 、必须严格遵守 http                                                                           

2.1 socket编程基础

        在Linux系统中,socket可用于表示进程间进行网络通信时使用的特殊文件类型,也可以用于表示socket编程中的一系列接口。socket本意为“插座”,常被称为套接字。当使用socket进行通信时,进程会先生成一个socket文件,之后再通过socket文件进行数据传递。

        Linux系统中将socket具体化为一种文件只是为了给用户提供与操作普通文件相同的接口,使用户可以通过文件描述符来引用和操作套接字。实际上,socket的本质为内存缓冲区形成的伪文件,与管道本质类似,不同的是,socket多用于与网络相关的进程通信

        在TCP/IP协议族中,使用IP地址和端口号可以唯一标识网络中的一个进程;socket通信原理如图10-4所示。

                                                图10-4  socket通信原理示意图        

      在网络通信中,socket一定是成对出现的。socket的缓冲区分为读写两个部分,每个socket都能接收和发送文件,一端的发送缓冲区会对应另一端的接收缓冲区

        对用户来说,不必了解socket文件的具体构成只需要掌握与socket相关的接口即可。socket接口位于应用层与TCP/IP协议族之间,是基于软件的抽象层,它与体系结构中各层的关系

如图10-5

                              图10-5 socket抽象层与体系结构的关系示意图

由图10-4可知,socket抽象层隐藏了协议的细节。用户不必了解TCP/IP协议的具体实现,只需要掌握socket编程接口,便能实现基于网络的进程通信。

2.1  socket编程接口

        Linux系统中常用的socket网络编程接口socket()、bind()、listen()、accept()、connect()、send()、recv()、close()。其中

①connect()与send()为客户端专用接口。

bind()、listen()、accept()及recv()为服务器端专用接口。

socket()与close()则由服务器与客户端共用。

下面将对这些接口逐一进行讲解。

(1)socket函数

socket()函数用于创建套接字,也可以说socket()函数用于打开网络通信端口。该函数类似于文件操作中的open()函数,若调用成功,返回一个文件描述符,之后应用程序可以采用socket通信中的读写函数在网络中收发数据。socket()函数存在于函数库sys/socket.h中,其声明如下:

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

socket()函数中

①第一个参数domain( 范围)用于指定通信域,选择通信时的地址族,其常用设置为AF_INETAF_UNIX,这些地址族都在头文件sys/socket.h中定义。其中AF_INET针对因特网,使用IPv4格式的IP地址,以此参数建立的socket可与远程的通信端连接并进行通信AF_UNIX则针对本地进程,以此参数建立的socket可在本地系统进程间进行通信

②第2个参数type用于指定socket的类型,其常用取值分别为SOCK_STRENAMSOCK_DGRAMSOCK_RAM。其中

SOCK_STREAM表示套接字使用TCP协议,提供按顺序、可靠、双向、面向连接且基于比特流的通信;

SOCK_DGRAM表示套接字使用UDP协议提供定长、不可靠、无连接且基于数据报的通信

SOCK_RAM表示套接字使用ICMP协议,提供单一的网络访问,一般用于开发人员需要自行设置数据格式或参数。

③第三个参数protocol一般设置为0,表示使用默认协议

socket函数若调用失败会返回-1并设置errno

(2)bind()    (捆绑)

bind()函数用于服务器端。服务器的网络地址和端口号通常固定不变,客户端程序得知服务器程序的地址和端口号后,可主动向服务器请求连接。因此服务器需要调用bind()函数进行地址绑定

bind()函数存在于函数库sys/socket.h中,其声明如下:

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

①bind()函数中的参数sockfd指代socket文件的文件描述符,一般由socket()函数返回;

②参数addr指代服务器的通信地址,其本质为struct sockaddr结构体类型的指针。struct sockaddr结构体的定义如下:

 struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */
               in_port_t      sin_port;   /* port in network byte order */
               struct in_addr sin_addr;   /* internet address */
           };

/* Internet address. */
           struct in_addr {
               uint32_t       s_addr;     /* address in network byte order */
           };

 

struct sockaddr{

        sa_family_t sa_family;

        char sa_data[14];

};

该结构体中的成员sa_data[]表示进程地址

③第三个参数addrlen表示参数addr的长度。实际上addr参数可接受多种类型的结构体,而这些结构体的长度各不相同,因此需要使用参数addrlen额外指定结构体长度。例如亦可使用以下语句,定义一个struct sockaddr_in类型的结构体:

 struct sockaddr_in servaddr;             //结构体定义

bzero(&servaddr,sizeof(servaddr));   //结构体清0

servaddr.sin_family=AF_INET;          //设置地址类型为AF_INET

servaddr.sin_addr.s_addr=htonl(INADDR_ANY);    //设置网络地址为INADDR_ANY

servaddr.sin_port=htons(85);              //设置端口号为85

bind()若调用成功,则返回0;否则返回-1并设置errno

(3)listen()

listen函数仍用于服务器端,其功能为使已绑定的socket等待监听客户端的连接请求,并设置服务器同时可建立的连接数量。listen()函数存在于函数库sys/socket.h中,其声明如下:

int listen(int sockfd,int backlog);

listen()函数中

①参数sockfd表示socket文件描述符;

②参数backlog用于设置请求队列的最大长度

经典的服务器可同时服务于多个客户端,当有客户端发起连接请求时,服务器调用的accept()函数将返回并接受这个连接;

若发起连接请求的客户端过多,服务器来不及处理时,尚未建立连接的客户端就会等待连接状态。listen()函数中的参数backlog便用于限制建立的连接数量

listen()函数若调用成功则返回0;否则返回-1并设置errno。

(4)accept()

accept()函数在listen()函数之后使用,其功能为等待处理客户端的请求

当传输层使用TCP协议时,服务器与客户端在创建连接之前,先经过“三次握手”机制测试连接,获取客户端信息,处理连接请求;

 若已完成连接队列为空,accept()函数便会是服务器程序进入阻塞状态。accept()函数存在于函数库sys/socket.h中,其声明如下:

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

accept()函数的参数

①参数sockfd为listen()函数返回的监听套接字。

②参数addrlen是一个传入传出参数。传入时为函数调用时提供参数addr的长度,传出时为客户端地址结构体的实际长度。

accept()函数的返回值也是一个套接字,该套接字用于与本次通信的客户端进行数据交互。

(5)connect()

connect()函数用于客户端,该函数的功能为向服务器发起连接请求。connect()函数存在于函数库sys/socket.h中,其声明如下:

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

connect()函数的参数与bind()函数中参数的形式一致

区别在于bind()中的参数为客户端进程地址,且都表示服务器的地址。

connect()函数调用成功则返回0,否则返回-1并设置errno。

(6)send()

send()函数用于向处于连接状态的套接字中发送数据,该函数存在于函数库sys/socket.h中,其声明如下:

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

send()函数中的参数

①参数sockfd表示要发送数据的socket文件描述符;

②参数buf为指向要发送数据的缓冲区指针

③参数len表示缓冲区buf中的数据的长度

④参数flags表示调用的执行方式(阻塞/非阻塞),当flags设置为0时,可使用之前学习的write()函数替代send()函数。

成功返回0,失败返回-1并设置errno。

需要注意的是,以上函数调用成功并不表示接收端一定会接收到发送的数据。

(7)recv()

recv()函数用于从已连接的套接字中接收信息,该函数存在于函数库sys/socket.h中,其声明如下:

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

该函数的参数列表与send()函数的参数列表形式相同,代表的含义也基本对应,只是其参数sockfd表示用于接收数据的socket文件文件描述符。

(8)close()

close()函数用于释放系统分配给套接字的资源,该函数即文件操作中常用于关闭文件的函数,存在于函数库unistd.h中,其声明如下:

int close(int fd);

close()函数中的参数fd为文件描述符,当其用于socket编程中时,需要传入socket文件描述符。该函数调用成功则返回0,否则返回-1并设置errno。

1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
 

2.2 socket通信流程

        根据进程在网络通信中使用的协议,可将socket通信方式分为两种:一种是面向连接、基于TCP协议的通信;另一种是面向无连接,基于UDP协议的通信。

        这两种通信方式的流程大致相同,区别在于面向连接的通信需要进行连接。

 当使用面向连接的方式进行通信时,服务器和客户机

①先各自创建socket文件

②服务器端调用bind()函数绑定服务器端口地址

③服务器端通过接口listen()设置可建立连接的数量

④若客户端需要与服务器端进行交互,客户端会调用connect()函数,向已知的服务器地址端口发送连接请求并阻塞等待服务器应答

⑤服务器监听到连接请求后,会调用accept()函数试图进行连接

⑥若服务器端连接的进程数量未达到最大连接数,便成功建立连接,此后客户端解除阻塞,两端可正常进行通信;否则服务器忽略本次连接请求。

⑦最后当它想完成之后,双方各自调用close()函数,关闭socket文件,释放资源。

 当使用面向无连接的方式进行通信时,服务器和客户机同样

①先各自创建自己的socket文件

②再由服务器端调用bind()绑定服务器端口地址。

③之后通信双方可直接开始通信,需要注意的是,因为服务器和客户机并未建立连接,所以客户端每次向服务器发送数据时,都要额外指定服务器的端口地址。同样的,若服务器需要向客户端发送数据,服务器也需要额外指定客户端的端口地址。

④通信结束后,通信双方需调用close()函数,关闭socket文件,释放资源。

2.3 网络编程相关知识

1.网络字节序

将数据的高字节保存在内存的低地址,将数据的低字节保存在内存的高地址,这种存放方式称为大端模式;相反地,若将数据的高字节保存在内存的高地址,将数据的低字节保存在内存的低地址,则这种存放方式称为小端模式。磁盘文件中的多字节数据相对于文件中的偏移地址有大端、小端之分,内存中的多字节数据相对于内存地址也有大端、小端之分;同样地,网络数据流同样有大端小端之分。

        TCP/IP协议规定,网络数据流应采用大端模式存储,即低地址存储高字节,高地址存储低字节。

举例说明:假设从网络端获取了一个16位的数据0x1121,即十进制的4385,那么发送端主机在发送该数据时,会先发送低地址的数据0x21,再发送高地址的数据0x11;假设接收端为此数据分配的地址为0和1,高字节的数据0x11将被放在低地址0所对应的空间,低字节的数据0x21将被存放到高地址1所对应的空间。

        但是,若发送端主机采用小端模式存储,那么这16位的数据会被解释为0x2111,即十进制的8465,这显然是不正确的。因此,为了保证网络程序的可移植性,使同样的代码可以在大端和小端设备上都能正常编译并运行,发送端的主机在将数据填充到发送缓冲区之前需要进行字节序转换。       

为了进行转换 字节序 提供了转换的函数 这些都在函数库arpa/inet.h有下面四个

htons把unsigned short类型从主机序转换到网络序

uint16_t htons(uint16_t hostshort);

htonl 把unsigned long类型从主机序转换到网络序

uint32_t htonl(uint32_t hostlong);

ntohs 把unsigned short类型从网络序转换到主机序

uint32_t ntons(uint16_t netshort);

ntohl 把unsigned long类型从网络序转换到主机序

uint16_t ntonl(uint32_t netlong);

        以上函数的函数名中的h代表主机host,n表示网络network,1表示32位长整型,s表示16位短整型。其中htonl()将无符号整数参数hostlong从主机字节序转换为网络字节序;htons()函数将无符号短整型参数hostshort从主机字节序转换为网络字节序。需要注意的是,若主机使用大端模式,则这些函数不作转换,将参数原样返回;也就是说,只有主机使用小端模式时,参数的字节顺序才会被修改。

IPv4,是互联网协议(Internet Protocol,IP)的第四版,也是第一个被广泛使用,构成现今互联网技术的基础的协议。1981年 Jon Postel 在RFC791中定义了IP,Ipv4可以运行在各种各样的底层网络上,比如端对端的串行数据链路(PPP协议SLIP协议) ,卫星链路等等。局域网中最常用的是以太网。

目前的全球因特网所采用的协议族是TCP/IP协议族。IP是TCP/IP协议族中网络层的协议,是TCP/IP协议族的核心协议。目前IP协议的版本号是4(简称为IPv4,v,version版本),它的下一个版本就是IPv6。IPv6正处在不断发展和完善的过程中,它在不久的将来将取代目前被广泛使用的IPv4。据国外媒体报道,欧盟委员会希望于2010年前将欧洲其成员国境内四分之一的商业和政府部门以及家用网络转换成IPv6标准。 美国已经开始对已经与网络服务商签订IPv6协议的政府部门给与有条件的奖励政策。而欧盟希望跟随美国的步伐,促使其成员国的政府部门在这次转型过程中起到带头作用。

地址格式

IPv4地址可被写作任何表示一个32位整数值的形式,但为了方便人类阅读和分析,它通常被写作点分十进制的形式,即四个字节被分开用十进制写出,中间用点分隔。

下表展示了几种不同的格式:

格式从点分十进制转换
点分十进制192.0.2.235不适用
点分十六进制0xC0.0x00.0x02.0xEB每个字节被单独转换为十六进制
点分八进制0300.0000.0002.0353每个字节被单独转换为八进制
十六进制0xC00002EB将点分十六进制连在一起
十进制3221226219用十进制写出的32位整数
八进制030000001353用八进制写出的32位整数

此外,在点分格式中,每个字节都可用任意的进制表达。如,192.0x00.0002.235是一种合法(但不常用)的表示。

2.IP地址转换函数

        常见的IP地址格式类似192.168.10.1,这是一个标准的IPv4格式的地址,但这种格式是为了方便用户对其进行操作。若要使计算机能够识别,需要先将其由文本格式转换为二进制格式。

 早期Linux系统中常使用以下函数来转换IP地址:

int inet_aton(const char *cp,struct in_addr *inp);

in_addr_t inet_addr(const char *cp);

char *inet_ntoa(struct in_addr in);

但以上函数只能处理IPv4的IP地址,且它们都是不可重入函数。

在 实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果这个函数不幸被设计成为不可重入的函数的话,那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。那么什么是可重入函数呢?所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。不可重入函数在 实时系统设计中被视为不安全函数。

满足下列条件的函数多数是不可重入的:

1) 函数体内使用了 静态的 数据结构;

2) 函数体内调用了malloc()或者free()函数;

3) 函数体内调用了标准I/O函数。

如今Linux编程中常用inet_pton()和inet_ntop()来转换IP地址,这两个函数不但能转换IPv4格式的地址in_addr,还能转换IPv6格式的地址in_addr6.它们存在于函数库arpa/inet.h中,函数定义如下:

int inet_pton(int af,const char *src,void *dst);

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

函数inet_pton()会先将字符串src转换af地址族中的网络地址结构,进而将转换后的网络地址结构存储到参数dst所指的缓冲区中,其中参数af的值必须是AF_INETAF_INET6

函数inet_ntop()会将af地址族中的网络地址结构src转换为字符串,再将获得的地址字符串存储到参数dst所指的缓冲区中。

以上两个函数需要转换IPv4和IPv6这两种形式的地址,因此用来传递地址的参数类型为void*。

3. sockaddr数据结构

IPv4和IPv6的地址格式定义在netinet/in.h中:

①IPv4地址用结构体sockaddr_in表示,该结构体中包含16位的端口号和32位的IP地址

struct sockaddr_in {

short int sin_family; /* Address family */

unsigned short int sin_port; /* Port number */

struct in_addr sin_addr; /* Internet address */

unsigned char sin_zero[8]; /* Same size as struct sockaddr */

};

sin_family:指代协议族,在socket编程中只能是AF_INET

sin_port:存储端口号(使用网络字节顺序

sin_addr:存储IP地址,使用in_addr这个数据结构

sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节

而其中in_addr结构的定义如下:

typedef struct in_addr {

union {

struct{ unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b;

struct{ unsigned short s_w1, s_w2;} S_un_w;

unsigned long S_addr;

} S_un;

IN_ADDR;

阐述下in_addr的含义,很显然它是一个存储ip地址的共用体有三种表达方式:

第一种用四个字节来表示IP地址的四个数字;

第二种用两个双字节来表示IP地址;

第三种用一个长整型来表示IP地址。

给in_addr赋值的一种最简单方法是使用inet_addr函数,它可以把一个代表IP地址的字符串赋值转换为in_addr类型,如addrto.sin_addr.s_addr=inet_addr("192.168.0.2");

反函数是inet_ntoa,可以把一个in_addr类型转换为一个字符串

②IPv6地址用结构体sockaddr_in6表示,该结构体中包含16位的端口号、128位的IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用结构体sock_addr_un表示。

各种socket地址结构的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6、UNIX Domain Socket的地址格式定义在sys/un.h中,用结构体sock_addr_un表示。

        各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度,后16位表示地址类型。

IPv4、IPv6和UNIX Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种的sockaddr结构体,例如bind()、accept()、connect()等函数,这些函数的参数应该设计成void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要进行强制转换,例如:

struct sockaddr_in servaddr;

bind(listen_fd,(struct sockaddr *)&servaddr,sizeof(servaddr));

3.socket网络编程实例

        掌握socket编程中的常用接口,以及基于TCP协议UDP协议的网络通信流程。

下面通过两个实例来展示socket网络编程实战

3.1 基于TCP的网络编程

        在创建socket时,若将socket()函数中参数type设置为SOCK_STREAM,程序将采用TCP传输协议,先使通信双方建立连接,再以数据段的形式传输数据。

    案例10-1:实现基于TCP协议的网络通信,其中:

①客户端的功能是从终端获取一个字符串发送给服务器,然后接收服务器返回的字符串并打印

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc,char *argv[])
{
	struct sockaddr_in servaddr;    //定义服务器地址结构
	char buf[MAXLINE];
	int sockfd,n;
	char *str;
	if(argc != 2){
		fputs("usage:./client message\n",stderr);
		exit(1);
	}
	str=argv[1];
    //创建客户端套接字文件
	sockfd=socket(AF_INET,SOCK_STREAM,0);
	//初始化服务器端口地址
	bzero(&servaddr,sizeof(servaddr));
	servaddr.sin_family=AF_INET;
	inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
	servaddr.sin_port=htons(SERV_PORT);
    //请求连接
	connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    //发送数据
	send(sockfd,str,strlen(str),0);
    //接收服务器返回的数据
	n=recv(sockfd,buf,MAXLINE,0);
    printf("Response from server:\n");
    //将服务器返回的数据打印到终端
	write(STDOUT_FILENO,buf,n);
    //关闭连接
	close(sockfd);
	return 0;
}

②服务器的功能是接收从客户端发来的字符,将每个字符转换为大写再返回给客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#define MAXLINE 80                         //最大数据长度
#define SERV_PORT 6666                     //服务器端口号
int main(void)
{
	struct sockaddr_in servaddr,cliaddr;   //定义服务器与客户端地址结构体
	socklen_t cliaddr_len;                 //客户端地址长度
	int listenfd,connfd;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i,n;
    //创建服务器端套接字文件
	listenfd=socket(AF_INET,SOCK_STREAM,0);
    //初始化服务器端口地址 
	bzero(&servaddr,sizeof(servaddr));    //将服务器端口地址清0
	servaddr.sin_family=AF_INET;
	servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
	servaddr.sin_port=htons(SERV_PORT);
    //将套接字文件与服务器端口地址绑定
	bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
	//监听,并设置最大连接数为20
	listen(listenfd,20);
	printf("Accepting connections...\n");
    //接收客户端数据,并处理请求
	while(1){
		cliaddr_len=sizeof(cliaddr);
		connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len);
		n=recv(connfd,buf,MAXLINE,0);
		printf("received from %s at PORT %d\n",
		inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),
		ntohs(cliaddr.sin_port));
		for(i=0;i<n;i++)
			buf[i]=toupper(buf[i]);
		send(connfd,buf,n,0);
        //关闭连接
		close(connfd);
	}
	return 0;
}

服务器端                                                                客户端

简单时间服务器的编写

服务器端

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAX_LISTQUEUE 5
int main(int argc,char *argv[])
{
	int listenfd,sockfd;
	struct sockaddr_in server,client;
	char buf[200];
	socklen_t len;
	int timep;

	listenfd=socket(AF_INET,SOCK_STREAM,0);
	if(listenfd<0){
		perror("create socket fail.");
		return -1;
	}

	bzero((void *)&server,sizeof(server));
	server.sin_family=AF_INET;
	server.sin_port=htons(8888);
	server.sin_addr.s_addr=htonl(INADDR_ANY);

	len=sizeof(struct sockaddr);
	if(bind(listenfd,(struct sockaddr *)&server,len)<0){
		perror("bind error");
		return -1;
	}

	listen(listenfd,MAX_LISTQUEUE);
	while(1){
		sockfd=accept(listenfd,(struct sockaddr *)&client,&len);
		if(sockfd<0){
			perror("accept error");
			return -1;
		}

		timep=time(NULL);
		snprintf(buf,sizeof(buf),"%s",ctime((const time_t *)&timep));
		write(sockfd,buf,sizeof(buf));
		printf("Bytes:%d\n",strlen(buf));
		close(sockfd);
	}
	return 0;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc,char *argv[])
{
	struct sockaddr_in servaddr;
	int sockfd;
	char buf[100];
	int bytes;

	if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
	{
		perror("socket error");
		return -1;
	}

	bzero((void *)&servaddr,sizeof(servaddr));
	servaddr.sin_family=AF_INET;
	servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
	servaddr.sin_port=htons(8888);

	if(connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))<0)
	{
		perror("connest error");
		return 0;
	}

	bytes=read(sockfd,buf,100);
	if(bytes<0)
	{
		perror("read error");
		return -1;
	}
	if(bytes==0)
	{
		printf("Server close connection.\n");
		return -1;
	}

	printf("Read bytes %d\n",bytes);
	printf("Time:%s\n",buf);

	close(sockfd);
}

服务器端                                                                客户端

基于UDP的网络通信

        在创建socket时,若将socket()函数中的参数type设置为SOCK_DGRAM,程序将采用UDP传输协议,以数据包的形式传输数据。

案例10-2:实现基于UDP的网络通信,其中:客户端的功能是从终端获取一个字符串发送给服务器,接收服务器返回的字符串并打印;服务器的功能是接收从客户端发来的字符,将每个字符转化为大写再返回给客户端。

        服务器程序保存在文件udpserver.c中,客户端程序保存在udpclient.c中。案例实现如下:

(1)udpserver.c——服务器端

#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <ctype.h>
#define MAXLINE 80       //最大数据长度
#define SERV_PORT 6666   //服务器端口号 
int main(void)
{
	struct sockaddr_in servaddr,cliaddr;//定义服务器与客户端地址结构体                                              
	socklen_t cliaddr_len;              //客户端地址长度
    int sockfd; 
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int n,i;
    sockfd=socket(AF_INET,SOCK_DGRAM,0); //创建服务器端套接字文件
    //初始化服务器端口地址
	bzero(&servaddr,sizeof(servaddr));         //地址结构体清0
	servaddr.sin_family=AF_INET;               //指定协议族  
	servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址
    servaddr.sin_port=htons(SERV_PORT);        //指定端口号 
	//绑定服务器端口地址 
	bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
	printf("Accepting connections...\n");
//ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
//const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
//ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
    //数据传输
	while(1){
		cliaddr_len=sizeof(cliaddr);
	    //接收数据	
		n=recvfrom(sockfd,buf,MAXLINE,0,(struct sockaddr*)&cliaddr,&cliaddr_len);
		if(n==-1)
			perror("recvfrom error");
		printf("received from %s at PORT %d\n"
			,inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),
			ntohs(cliaddr.sin_port));
        //服务器端操作,小写转大写
		for(i=0;i<n;i++)
			buf[i]=toupper(buf[i]);
		n=sendto(sockfd,buf,n,0,(struct sockaddr *)&cliaddr,sizeof(cliaddr));
		if(n==-1)
			perror("sendto error");
	}
	close(sockfd);
	return 0;
}

(2)udpclient.c——客户端

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <ctype.h>
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc,char *argv[])
{
	struct sockaddr_in servaddr;
	int sockfd,n;
	char buf[MAXLINE];
	
	sockfd=socket(AF_INET,SOCK_DGRAM,0);
	bzero(&servaddr,sizeof(servaddr));
	servaddr.sin_family=AF_INET;
	inet_pton(AF_INET,"127.0.0.0",&servaddr.sin_addr);
	servaddr.sin_port=htons(SERV_PORT);
    //发送数据到服务器
	while(fgets(buf,MAXLINE,stdin)!=NULL){
		n=sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&servaddr,sizeof(servaddr));
	if(n==-1)
		perror("sendto error");	
	//接收服务器返回的数据
	n=recvfrom(sockfd,buf,MAXLINE,0,NULL,0);
	if(n==-1)
		perror("recvfrom error");
	//将接收到的数据打印到终端
	write(STDOUT_FILENO,buf,n);	
	}
	close(sockfd);
	return 0;
}

编译以上两段代码,打开两个终端窗口,先执行服务器端程序,再执行客户端程序,同时输入要转化的字符串。客户端和服务器端对应的终端中分别打印如下信息:

服务器端:

Accepting connection...

received from 127.0.0.1 at 33059

received from 127.0.0.1 at 33059

客户端:

hello

HELLO

world

WORLD

4.socket本地通信

        socket原本是为网络通信设计的,但后来在socket框架的基础上发展出一种IPC(进程通信)机制,即UNIX Domain Socket,专门用来实现使用socket实现的本地进程通信。其实socket原本便能实现本地通信功能,但使用Domain机制时,数据不需要再通过网络协议族,也无须进行拆包、计算校验和以及应答等网络通信中涉及的一系列操作,只需要将应用层的数据从本地的一个进程拷贝到另一个进程。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值