您的位置:首页 > 理论基础 > 计算机网络

Linux C复习历程(七)——网络编程

2019-01-19 16:59 1001 查看

一、Linux网络模型

Linux中网络栈的介绍一般分为四层的Internet模型:

TCP/IP协议族体系结构及主要协议:

1、TCP/IP协议族

TCP/IP实际上是一个协同工作的通信家族,为网络通信提供通路。为方便讨论TCP/IP协议族,大体上分为三部分:

Internet协议(IP)。

传输控制协议(TCP)和用户数据报协议(UDP)。

处于TCP和UDP之上的一组应用协议。它们包括:Telnet,文件传送协议(FTP),域名服务协议(DNS)和简单的邮件传送程序(SMTP)等。

2、数据链路层

数据链路层实现了网卡接口的网络驱动程序,以处理数据在网络媒介上(比如以太网)上的传输。不同的物理网络层具有不同的电器特性,网络驱动程序隐藏了这些细节,为上层协议提供了统一的接口

3、网络层

网络层实现数据包的选路和转发。WAN通常使用众多分级的路由器来连接分散的主机或LAN,因此,通信的两台主机一般不是直接连接的,而是通过多个中间节点连接的。网络层的任务就是选择这些节点,以确定两台主机间的通信路径。同时,网络层对上层协议隐藏了网络拓扑连接的细节,使得在传输层和网络应用程序看来,通信的双方是直接相连的。

4、传输层

传输层为两台主机上的应用程序提供端到端(end to end)的通信。与网络层使用的逐跳的通信方式不同,传输层只关心通信的起始端和目的端,而不关心数据包的中转过程

5、应用层

应用层负责处理应用程序的逻辑

应用层协议很多:ping:应用程序,不是协议,调试网络环境;

                             telnet:远程登录协议。

                             DNS:机器域名到ip的转换。

                             HTTP:超文本传输协议,是一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送文档                                                         的数据传送协议。

                             DHCP:动态主机配置协议。

6、数据封装

应用层程序在发送到物理网络之前,将沿着协议栈从上往下依次传递。每层协议都将在上层协议的基础上加上自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程称为封装。

7、IP协议:

IP的主要目的是为数据输入/输出网络提供基本算法,为高层协议提供无连接的传送服务。这意味着在IP将数据交给接收站以前不在传输站点和接收站点之间建立对话。它只是封装和传递数据,但不向发送者或接收者报告包的状态,不处理所遇到的故障

IP包由IP协议头协议数据两部分构成。

8、TCP协议

TCP协议(传输控制协议)为应用层提供可靠的面向连接的、基于流(stream)的服务。TCP协议使用超时重传数据确认等方式来确保数据包被正确发送到目的地,因此TCP服务是可靠的。使用TCP协议通信的双方必须先建立TCP连接,并且在内核中为该连接的双方必须先建立TCP连接,并且在内核中为该连接维持一些必须的数据结构。当通信结束时,双方必须关闭连接以释放这些内核数据。

(1)TCP协议头部结构

(2)TCP三次握手

所谓三次握手(Three—way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送三个包。

这个东西还是挺有搞头的,把这一大块整理完后,再把三次握手具体过程理一理。

(3)TCP四次挥手

TCP连接的删除需要发送四个包,因此称为四次挥手(four—way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。

9、UDP协议

UDP协议(用户数据报协议),它与应用层TCP协议完全相反。提供不可靠、无连接和基于数据报的服务。不可靠意味着UDP协议无法保证数据从发送端正确的发送到接收端。如果数据在中途丢失,或者目的端通过数据校验发现数据错误而将其丢弃,则UDP协议的应用程序通常要自己处理数据确认、超时重传等逻辑性。

 

二、Linux网络编程基础

1、网络套接字socket

Linux中的网络编程通过Socket(套接字)实现,Socket是一种文件描述符

(1)socket的三种类型

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

数据报套接字(SOCK_DGRAM):数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错,它使用数据报协议UDP

原始套接字(SOCK_RAW):原始套接字允许使用IP协议,主要用于新的网络协议的测试等。 一般用于网络测试。

2、地址结构

socket程序设计宏,struct sockaddr用于保存socket地址信息

struct sockaddr

{

              unsigned short sa_family;   /*地址族*/

               char sa_data[14];               /*14字节的协议地址,包含该socket的IP地址和端口号*/

}

sa_family:协议族,采用“AF_XXX”的形式,如AF_INET(IPV4协议)

sa_data:14字节的特定协议地址。

socket程序设计中,struct sockaddr_in 同样用于保存socket地址信息

struct sockaddr_in

{

              short int sin_family;   /*Internet地址族*/

               unsigned short int sin_port;  /*端口号*/

               struct in_addr sin_addr;   /*IP地址*/

               unsinged char sin_zero[8];   /*填0以保持和strruct sockaddr同样大小*/

};

IP地址结构:

struct in_addr  //网络IP的类型

{

               unsigned  long s_addr;    /*32位的地址*/

}

在实际编程中,一般不针对sockaddr数据结构进行操作,而是使用与sockaddr_in数据结构。

3、字节序转换

(1)字节序概念

不同类型的CPU对变量的字节存储顺序可能不同;有的系统是高位在前,低位在后,而有的系统是低位在前,高位在后。在网络的数据顺序一定是要统一的。所以当内部字节存储顺序和网络字节顺序不同时,就一定要进行转换

如32bit的整数(0x01234567)从地址0x100开始:

V小端字节序(高位放在低地址)

V大端字节序(高位放在高地址)

(2)网络字节序:

网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型,操作系统等无关。从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式

(3)字节序转换函数:

#include<netinet/in.h>

uint16_t htons(unit16_t host16bit);  //把unsigned int 类型从主机序到网络序

uint32_t htonl(unit32_t host32bit);   //把unsigned long 类型从主机序到网络序

uint16_t ntohs(unit16_t net16bit);  //把unsigned short 类型从网络序到主机序

unit32_t ntohl(unit32_t net32bit);   //把unsigned long 类型从网络序到主机序  

h--host  n--network  s--short   l--long 通常16位的IP端口号用s代表。而IP地址用I代表。

4、IP与主机名

在网络上标识一台机器可以用IP,也可以使用主机名。

#include<netdb.h>

struct hostent * getnostbyname(const char * hostname);  // 实现主机名到IP地址的转换

函数返回值:

struct hostent 

{

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

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

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

            int h_length;         //主机的地址列表长度

            char **h_addr_list;  //主机的IP地址列表(32位网络地址)

}

#define  h_addr  h_addr_list[0]   //主机的首个IP地址

5、IP地址转换

IP地址通常由数字加点的形式表示,而struct in_addr中使用的IP地址是32位整数表示的,为了转换可以使用如下两个函数。

#include<netdb.h>

int iner_aton(const char * cp, struct in_addr *inp);   //将a.b.c.d形式的IP转换为32位的IP

char * inet_ntoa(struct in_addr in);        //将32位的IP转化为a.b.c.d的形式

 

三、socket网络编程

1、socket核心函数

(1)socket

#include<sys/types.h>

#include<sys/socket.h>

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

sockt函数对应于文件的打开操作(Linux一切皆文件),函数用于创建一个socket描述符。把它作为参数可以进行读,写等操作。

参数含义:sdomain:协议域,又称协议族。常用协议:AF_INET(IPV4协议)、AD_INET6(IPV6协议)、AF_LOCAL、                                                                AF_ROUTE等。

                              type:指定socket的类型。上文有介绍,常用的有三种,SOCK_STREAM,SOCK_DGRAM。

                             prorocol:指定协议当protocol为0时会自动选择type类型对应的默认协议

上面的type和protocol不可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

(2)bind

bind()函数将一个地址族中的特定地址赋给socket。

#include<sys/types.h>

#include<sys/socket.h>

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

sockfd: socket描述字,通过socket()函数创建,唯一标识一个socket。bind()函数就是给这个描述字绑定一个名字。

addr:这个结构体指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket的地址协议族的不同而不同,IPV4对应的IP地址是sockaddr_in上文有具体说明这个结构体。

为什么服务器需要绑定,而客户端不需要?

通常服务器在启动时需要绑定一个地址(地址加端口号)用于提供服务,客户端可以通过这个地址对服务器进行连接。而客户端则不需要,由系统自动分配端口号和自身IP地址组合即可。所以服务器在监听前需要进行绑定,而客户端不需要,在connect()时会由系统自动生成。

为什么有时候会出现bind()出错的情况,如何解决?

当clinet终止时自动关闭socket描述符,server的TCP连接收到的client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间才能回到CLOSE的状态,当ctrl-C结束server时,server是主动关闭的一方,TIME_WAIT期间不能再次监听同样的server端口。

在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开指的是fd(127.0.0.1:8000)没有完全断开,而我们重新监听的是sockfd(0.0.0.0:8000),虽然是占用同一个端口,但IP地址不同,fd对应的是不某个客户端通讯的一个具体的IP地址,而sockfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。

在socket和bind之间插入如下代码。

int opt = 1;

setsockopt(sockfd,SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

(3)listen和connect

服务器在调用socket()、bind()之后调用listen()来监听这个socket,如果客户端这时调用connect发出连接,服务器将会收到这个请求。

#include<sys/types.h>

#include<sys/socket.h>

int listen(int sockfd, int backlog);

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

listen的第一次参数为监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型,等待客户的连接请求。

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数与TCP服务器建立连接。

(4)accept

服务器监听到客户端通过connect发出的连接请求后,就将调用accept()函数来接受这个请求,这样连接就建立好了。之后就可以进行网络I/O操作,类同于普通文件的读写I/O操作。

#include<sys/types.h>

#include<sys/socket.h>

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

函数参数结构与connect,第一个参数是服务器socket()产生的sockfd,称为监听套接字,用于监听新的连接。第二个参数用于返回客户端的IP地址信息。

返回值是一个全新的文件描述符,这个文件描述符用于新建立的该连接的信息传输。

(5)read()/write()

至此,服务器与客户端已建立起连接。可以使用网络I/O进行读写操作,实现网络中不同进程之间的通信。

#include <unistd.h> 

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

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

read用于从sockfd中读取内容,读成功时,read返回实际所读字节数, 小于0表示出错。

write函数将buf中的count自己内容写入sockfd中。失败返回-1。

(6)sendto()/recvfrom()

这两个函数用于基于无连接的网络传输协议,每次进行信息传输,需要注明接收方或者发送方的IP地址信息。

ssize_t sendto(int sockfd,  const vod *buf,  size_t  len, int  flags, const struct sockaddr * dest_addr, socklen_t  addrlen); 

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

(7)close()

在完成了读写操作后,关闭相应的socket描述符,与文件操作的fclose()类似。

#include <unistd.h> 

int close(int fd);

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

2、基于TCP的网络编程

(1)基于TCP—服务器

(a)创建一个socket,用函数socket()

(b)绑定IP地址、端口等信息到socket上,用函数bind()

(c)监听套接字,设置允许的最大连接数,用函数listen()

(d)接收客户端发来的连接请求,用函数accept()

(e)收发数据,用函数send()和recv(),或者read()和write()

(f)关闭网络连接

(2)基于TCP—客户端

(a)创建一个socket,用函数socket()

(b)设置要连接的对方IP地址和端口等属性。

(c)连接服务器,用函数connect()

(e)收发数据,用函数send()和recv(),或者read()和write()

(f)关闭网络连接

TCP模型

3、基于UDP的网络编程

(1)基于UDP—服务器

(a)创建一个socket,用函数socket()

(b)绑定IP地址,端口等信息到socket上,用函数bind()

(c)循环接收数据,用函数recvfrom()

(d)关闭网络连接

(2)基于UDP—客户端

(a)创建一个socket,用函数socket()

(b)绑定IP地址,端口等信息到socket上,用函数bind()

(c)设置连接方的IP地址和端口等信息

(d)发送数据,用函数sendto()

(e)关闭网络连接

UDP模型

4、服务器模型

在网络程序里,一般来说都是许多客户对应一个服务器,为了处理客户的请求,对服务端的程序就提出特殊的要求。目前最常用的服务器模型有:

循环服务器:服务器在同一个时刻只可以相应一个客户端的请求。

并发服务器:服务器在同一个时刻可以相应多个客户端的请求。

(1)UDP循环服务器

UDP循环服务器实现的方法:UDP服务器每次从套接字上读取一个客户端的请求->处理->将结果返回给客户端

socket(....);

bind(...);

while(1)

{

              recvfrom(...);

              process(....);

               sendo(....);

}

因为UDP是非面向连接的,没有一个客户端可以总是占用服务器,所有服务器对每一个客户的要求总能满足。

(2)TCP并发服务器

并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是由服务器创建一个子进程(线程)来处理。算法如下:

socket(.....);

bind(.....);

listen(.....);

while(1)

{

             accept(....);

             if(fork(...) == 0)   {

             process(.....);

              exit();

             }

              close(.....);

}

缺点:创建子进程处理客户端请求对资源内存消耗很大。

5、多路复用I/O

阻塞函数在完成其他指定的任务以前不允许程序继续向下执行。例如:当服务器运行到accept语句时,而没有客户请求连接,服务器就会停在accept语句上等待连接请求的到来。这种情况称为阻塞。例如,如果你希望服务器仅仅检查是否有客户在等待连接,有就接受连接,否则就继续做其他事情,则可以通过select系统调用来实现。除此之外,select还可以同时监视多个套接字。

SELECT:

int select(int maxfd, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, const  struct timeval * timeout);

参数:

Maxfd :文件描述符的范围,比待检的最大描述符大一即可。

Readfds:被读监控的文件描述集

Writefds:被写监控的文件描述符集

Exceptfds:被异常监控的文件描述符集

Timeout:定时器,0:非阻塞 , NULL:阻塞, 正整数:等待的最大时间,即函数在timeout时间是阻塞的、

返回值:

正常情况返回满足要求的文件描述符个数;

经过了timeout等待时间仍无文件满足,返回值为0;

如果select 被某个信号中断,它将返回-1并设置error为EINTR;

出错,返回-1并设置相应的error。

使用步骤:

(1)设置要监控的文件

(2)调用select开始监控

(3)判断文件是否发送变化

系统提供了4个宏对描述符集进行操作:  

 #include <sys/select.h>  

void FD_SET(int fd, fd_set *fdset)    //将文件描述符fd添加到文件描述符集fdset中

void FD_CLR(int fd, fd_set *fdset)    //从文件描述符集fdset中清除文件描述符fd 

void FD_ZERO(fd_set *fdset)      //清空文件描述符集fdset

void FD_ISSET(int fd, fd_set *fdset)   //在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发生了变化

 

 

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: