文档:十三.linux网络之Socket网络编程基础(...
链接:http://note.youdao.com/noteshare?id=80df9f088e86efaa65539c40b23f5594&sub=E3F0055D389F4939ADB5DD022FB8B39A
一.linux网络编程框架
1、网络是分层的
(1)OSI 7层模型
(2)网络为什么要分层:网络通信功能强大,故技术角度也比较复杂,所以才会分层。
(3)网络分层的具体表现:上节已经简单梳理过。
2、TCP/IP协议引入(网络分层模型OSI和TCP/IP四层模型)
(1)TCP/IP协议是用的最多的网络协议实现
(2)TCP/IP分为4层,对应OSI的7层
1.链路层(数据链路层/网络接口层):包括操作系统中的设备驱动程序、计算机中对应的网络接口卡
2.网络层(互联网层):处理分组在网络中的活动,比如分组的选路。
3.运输层:主要为两台主机上的应用提供端到端的通信。
4.应用层:负责处理特定的应用程序细节。
(3)我们编程时最关注应用层,了解传输层,网际互联层和网络接入层不用管
3、BS和CS
(1)CS架构介绍(client server,客户端----服务器结构)
(2)BS架构介绍(broswer server,浏览器----服务器结构)
B/S架构更多的时候是使用了HTTP协议、而C/S架构更多的时候使用的WinSocket协议(TCP、UDP)
二.TCP协议的学习1
1、关于TCP理解的重点
(1)TCP协议工作在传输层,对上服务socket接口,对下调用IP层
(2)TCP协议面向连接,通信之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的是同步连接双方的序列号和确认号并交换 TCP窗口大小信息。
为什么需要三次握手?
为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误
(3)TCP协议提供可靠传输,不怕丢包、乱序等。
2、TCP如何保证可靠传输???
- (1)TCP在传输有效信息前要求通信双方必须先握手,建立连接才能通信
- (2)TCP的接收方收到数据包后会ack(回应机制:接收方每次收到数据,都会
回返一个回应)给发送方,若发送方未收到ack会丢包重传。
- (3)TCP的有效数据内容会附带校验信息,以防止内容在传递过程中损坏
- (4)TCP会根据网络带宽来自动调节适配速率(滑动窗口技术演示如下:)
首先第一次发送数据这个时候的窗口大小是根据链路带宽的大小来决定的。我们假设这个时候窗口的大小是3
这里我们看到接收方发送的ACK=3(这是发送方发送序列2的回答确认,下一次接收方期望接收到的是3序列信号)。这个时候发送方收到这个数据以后就知道我第一次发送的3个数据对方只收到了2个。就知道第3个数据对方没有收到。下次在发送的时候就从第3个数据开始发。这个时候窗口大小就变成了2 ,这个时候发送方就发送2个数据了。
- (5)发送方会给各分割报文编号,接收方会校验编号,一旦顺序错误即会重传。
三.TCP协议的学习2
1、TCP的三次握手、四次挥手
(1)建立连接需要三次握手、关闭连接需要四次握手,整个过程如下图所示:
(2)建立连接的条件:最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器
TCP连接的建立(三次握手):
(转载部分注明:https://blog.youkuaiyun.com/qzcsu/article/details/72861891)
|
SYN,ACK,FIN存放在TCP的标志位,一共有6个字符,这里就介绍这三个:
关于握手和分手,主要还是SYN,FIN,ACK的变化,这才是重点!
SYN:代表请求创建连接,所以在三次握手中前两次要SYN=1,表示这两次用于建立连接。
FIN:表示请求关闭连接,在四次分手时,我们发现FIN发了两遍。这是因为TCP的连接是双向的,所以一次FIN只能关闭一个方向。
ACK:代表确认接受,从上面可以发现,不管是三次握手还是四次分手,在回应的时候都会加上ACK=1,表示消息接收到了,并且在建立连接以后的发送数据时,都需加上ACK=1,来表示数据接收成功。
seq:序列号,什么意思呢?当发送一个数据时,数据是被拆成多个数据包来发送,序列号就是对每个数据包进行编号,这样接受方才能对数据包进行再次拼接。
初始序列号是随机生成的,这样不一样的数据拆包解包就不会连接错了。(例如:两个数据都被拆成1,2,3和一个数据是1,2,3一个是101,102,103,很明显后者不会连接错误)
ack:这个代表下一个数据包的编号,这也就是为什么第二请求时,ack是seq+1,
过程详解:
- ①.TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
- ②.TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,表示要创建连接。同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
- ③.TCP服务器收到请求报文后,如果同意连接,则发出确认报文。(服务端接收到后,要告诉客户端:我接受到了!)故ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
- ④.TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
- ⑤.当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了
|
主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
注:这些握手协议已经封装在TCP协议内部,socket编程接口平时不用管
TCP连接的释放(四次挥手)
1.首先客户端请求关闭客户端到服务端方向的连接,这时客户端就要发送一个FIN=1,表示要关闭一个方向的连接(见上面四次分手的图)
2.服务端接收到后是需要确认一下的,所以返回了一个ACK=1
3.这时只关闭了一个方向,另一个方向也需要关闭,所以服务端也向客户端发了一个FIN=1 ACK=1
4.客户端接收到后发送ACK=1,表示接受成功
四次分手完成!
|
如果发送两次就可以建立连接话可能出现的问题:
如果一个连接请求在网络中跑的慢,超时了,这时客户端会从发请求,但是这个跑的慢的请求最后还是跑到了,然后服务端就接收了两个连接请求,然后全部回应就会创建两个连接,浪费资源!
如果加了第三次客户端确认,客户端在接受到一个服务端连接确认请求后,后面再接收到的连接确认请求就可以抛弃不管了。
三次握手时,建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
2、基于TCP通信的服务模式
(1)具有公网IP地址的服务器(或者使用动态IP地址映射技术)
(2)服务器端socket、bind、listen后处于监听状态
(3)客户端socket后,直接connect去发起连接。
(4)服务器收到并同意客户端接入后会建立TCP连接,然后双方开始收发数据,收发时是双向的,而且双方均可发起
(5)双方均可发起关闭连接
3、常见的使用了TCP协议的网络应用
(1)http、ftp
(2)QQ服务器
(3)mail服务器
三.socket编程接口介绍
1、Socket原理
Socket是应用层与TCP/IP协议族通信的中间软件抽象层是一种编程接口。Socket屏蔽了不同网络协议的差异支持面向连接(Transmission Control Protocol - TCPIP)和无连接(User Datagram Protocol-UDP 和 Inter-Network Packet Exchange-IPX)的传输协议。
2、建立连接
-
(1)socket
int socket(int domain, int type, int protocol);//创建一个Socket
函数参数: domain即协议域又称为协议族family。常用的协议族有AF_INET、AF_INET6、AF_LOCAL或称AF_UNIXUnix域socket、AF_ROUTE等等。协议族决定了socket的地址类型在通信中必须采用对应的地址如AF_INET决定了要用ipv4地址32位的与端口号16位的的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type指定socket类型。常用的socket类型有SOCK_STREAM(TCP协议)、SOCK_DGRAM(UDP协议)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol指定协议。常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。当protocol为0时会自动选择type类型对应的默认协议。 成功则返回一个网络文件描述符,错误则返回-1; |
socket函数类似于open,用来打开一个网络连接,如果成功则返回一个fd文件描述符(int类型),之后我们操作这个网络连接都通过这个网络文件描述符。
-
(2)bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 作用:bind绑定sockefd和当前电脑的ip地址&端口号 函数参数: sockfd即socket描述字通过socket函数创建得到唯一标识一个socket。 const struct sockaddr *指针指向要绑定给sockfd的协议地址。 addrlen:长度 正确返回0,错误返回-1 |
-
(3)listen
int listen(int sockfd, int backlog); 功能:设置sockfd套接字为监听套接字,listen一般用于监听端口 参数: sockfd参数即为要监听的socket描述字 backlog参数为相应socket可以排队的最大连接个数。 socket函数创建的socket默认是一个主动类型的listen函数将socket变为被动类型的等待客户的连接请求。 正确返回0,错误返回-1 |
-
(4)accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 功能:接收客户端的请求建立连接套接字 参数: 参数sockfd是监听套接字用来监听一个端口 参数addr用来接收客户端的协议地址,可以设置为NULL。 参数len表示接收的客户端的协议地址addr结构的大小,可设置为NULL 如果accept成功返回,则服务器与客户已经正确建立连接了,accept返回的套接字是一个fd(文件描述符),服务器通过accept返回的套接字来完成与客户的通信。 后面需要的读写操作就需要一个fd,这个fd就由accept来返回的! 注意:使用此函数时,会阻塞等待客户端来连接服务器 |
注意:socket返回的fd叫做监听fd,是用来监听客户端的,不能用来和任何客户端进行读写;accept返回的fd叫做连接fd,用来和连接那端的客户端程序进行读写。
-
(5)connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能:用于建立与指定socket的连接 参数: sockfd参数即为客户端的socket描述字 addr参数为服务器的socket地址 addrlen参数为socket地址的长度 返回值:成功执行后客户端通过调用connect函数来建立与TCP服务器的连接。 正确返回0,错误返回-1 |
3、数据发送和接收函数
-
(1)send和write
ssize_t send(int sockfd, const void *buf, size_t len, int flags); 功能: send函数向连接套接字sockfd发送内容 参数: sockfd参数表示发送到的连接套接字 buf参数表示要发送的内容所在的内存缓冲区 len参数表示要发送内容的长度 flags参数表示send的标识符一般为0 返回值:成功返回实际发送的字节数,出错返回-1 |
ssize_t write(int fd, const void *buf, size_t count); 功能: write函数是向连接套接字fd写入内容 参数:fd参数表示建立的连接套接字 buf参数表示要写入内容所在的内存缓冲区 count参数表示要写入的内容的大小 |
-
(2)recv和read
ssize_t recv(int sockfd, void *buf, size_t len, int flags); 功能: recv函数从连接套接字sockfd接收内容 参数: sockfd参数表示从哪个连接套接字接收内容 buf参数表示接收的内容存放的内存缓冲区 len参数表示接收内容的实际字节数 flags参数表示recv操作标识符一般为0 |
ssize_t read(int fd, void *buf, size_t count); 功能: read函数是负责从连接套接字fd中读取内容。 参数: fd是accept函数建立的连接套接字 buf是读取的内容存放的内存缓冲区 count是要读取的内容的大小 返回值:当读成功时read返回实际所读的字节数如果返回的值是0表示已经读到文件的结束小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的如果是ECONNREST表示网络连接出了问题。 |
-
(3)close
int close(int fd); 功能: 关闭断开连接套接字 参数: fd参数表示要断开的连接套接字 |
4、地址转换函数(了解即可)
-
(1)inet_aton函数、inet_addr函数、inet_ntoa函数
三个函数用于二进制地址格式与点分十进制之间的相互转换,但是仅仅适用于IPv4
原型:int inet_aton(const char *string, struct in_addr *addr); 功能:inet_aton()函数用于将点分十进制IP地址转换成网络字节序IP地址; 参数:(1)const char *IP:我们输入的点分十进制的IP地址; (2)struct in_addr* addr: 将IP转换为网络字节序(大端存储)后并保存在addr中; 返回值:如果这个函数成功,函数的返回值非零,如果输入地址不正确则会返回零; 头文件:sys/socket.h (Linux) |
原型:in_addr_t inet_addr(const char *cp); 功能:inet_addr()函数用于将点分十进制IP地址转换成网络字节序IP地址; 参数:二进制地址 返回值:如果正确执行将返回一个无符号长整数型数。如果传入的字符串不是一个合法的IP地址,将返回INADDR_NONE; 头文件:arpa/inet.h (Linux) |
原型:char *inet_ntoa (struct in_addr); 功能:inet_ntoa()函数用于网络字节序IP转化点分十进制IP; 函数形参: sruct in_addr { in_addr_t s_addr; }; in_addr是一个按网络顺序存储的IP地址。
返回值:若无错误发生,inet_ntoa()返回一个字符指针。否则的话,返回NULL。其中的数据应在下一个WINDOWS套接口调用前复制出来; 头文件:arpa/inet.h (Linux) |
-
(2)inet_ntop函数、inet_pton函数
具有相似的功能,并且同时支持IPv4和IPv6。
原型:const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 功能:inet_ntop()函数用于将网络字节序的二进制地址转换成文本字符串; 参数说明: *af:AF_INET或AF_INET6. *src:指向套接字地址结构中的二进制值。 *dst:指向转换后的字符串的存储位置。 *size:指定dst指向存储单元的大小. 在<netinet/in.h>中:#define INET_ADDRSTRLEN 16 //for IPv4 ;#define INET6_ADDRSTRLEN 46 //for IPv6.
返回值:若成功,返回地址字符串指针;若出错,返回NULL; 头文件:arpa/inet.h (Linux) |
原型:int inet_pton(int af, const char *src, void *dst); 功能:inet_pton()函数用于将文本字符串格式转换成网络字节序二进制地址; 参数说明:
*af:AF_INET(IPv4)或AF_INET6(IPv6). *src:指向要转换的字符串. *dst:指向存放网络字节序的二进制结果的地址。若为AF_INET(IPv4)则是struct in_addr xx 若为AF_INET6(IPv6).则是struct in6_addr xx
返回值:若成功,返回1;若格式无效,返回0;若出错,返回-1; 头文件:arpa/inet.h (Linux) |
5、表示IP地址相关数据结构
(1)都定义在 /usr/include/netinet/in.h
(2)typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型
(3)就是一个32位的IP地址。
struct in_addr
{
in_addr_t s_addr;
};
(4)IPV4的协议地址结构体
struct sockaddr_in {
sa_family_t sin_family; /* 设置地址族为IPv4或者IPv6 */
in_port_t sin_port; /* 定义服务器地址的端口号*/
struct in_addr sin_addr; /* IP地址*/
};
/* Internet address. */struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
(5)IPV6的协议地址结构体
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
(6)这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体。
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
常用的库函数如bind,accept,connect,sendto,recvfrom,getpeername及getsockname等,其函数原型中均以struct sockaddr *作为地址参数。
在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充。
问题: sockaddr_in , sockaddr , in_addr区别???
- sockaddr是通用的socket地址结构体
- sockaddr_in是Internet 和socket通用的IPV4地址结构体
- in_addr就是32位IP地址
四.IP地址格式转换函数测试
E:\Linux\3.AppNet\7.socket\7.1
1、inet_addr、inet_ntoa、inet_aton
#define IPADDR "192.168.1.102"
// 0x66 01 a8 c0
// 102 1 168 192
// 网络字节序,其实就是大端模式
//①inet_addr函数测试
int main()
{
in_addr_t addr=0;
addr=inet_addr(IPADDR);
printf("addr=0x%x.\n",addr);// 0x6601a8c0
return 0;
}
#define IPADDR "192.168.1.102"
int main()
{
struct in_addr addr = {0};
/* ④.inet_ntoa函数测试
addr.s_addr = 0x6601a8c0;
printf("ip addr = %s.\n",inet_ntoa(addr));//ip addr = 192.168.1.102.
*/
//⑤.inet_aton函数测试
inet_aton(IPADDR,&addr);
printf("addr = 0x%x.\n", addr.s_addr);// 0x6601a8c0
}
2、inet_pton、inet_ntop
#define IPADDR "192.168.1.102"
int main()
{
//③.inet_ntop函数测试
struct in_addr addr = {0};
char buf[50] = {0};
addr.s_addr = 0x6703a8c0;
inet_ntop(AF_INET, &addr, buf, sizeof(buf));
printf("ip addr = %s.\n", buf);//192.168.1.102
// ②inet_pton函数测试
int ret = 0;
struct in_addr addr = {0};
ret = inet_pton(AF_INET, IPADDR, &addr);
if (ret != 1)
{
printf("inet_pton error\n");
return -1;
}
printf("addr = 0x%x.\n", addr.s_addr);// 0x6601a8c0
}