(1)OSI 7层模型
(2)TCP/IP分为4层,对应OSI的7层
(3)我们编程时最关注【应用层】,了解传输层(TCP/UDP/TFTP),网际互联层和网络接入层不用管
TCP 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议
位于传输层
对数据包做进一步加工,如提供端口号等
向上服务socket接口(应该是应用层与传输层间的一个抽象层,是个编程接口API),向下调用IP网络层
IP 英特网协议Internet Protocol 位于网络层
规定了传输数据时的基本单元(数据包)和格式,数据包的递交方法和路由选择
接受有更底层网络接口层(例如以太网设备驱动程序)发来的数据包,并把该数据包发送到更高层-TCP层;相反,IP层也会 从TCP接受来的数据包传递到更底层
3.9.1.3、BS和CS
(1)CS架构介绍(client server, 客户端服务器架构)
(2)BS架构介绍(broswer server,浏览器服务器架构)
①TCP的服务器端socket基本流程socket->bind->listen->accept->send/recv->closesocket
②客户端基本流程socket->[bind->]->connect->send/recv->closesocket,其中客户端connect函数应该是和服务器端的listen函数相互作用,而不是accept函数。每当有一个客户端connect了,listen的队列中就加入一个连接,每当服务器端accept了,就从listen的队列中取出一个连接,转成一个专门用来传输数据的socket(accept函数的返回值),所以在服务器端程序中有两个socket,前者是用来接收客户端连接的socket,后者用于数据传输
服务器端
创建套接字函数:
用于打开一个网络连接
int socket(int domain,int type,int protocol);
①int domain:通信协议族(比如是IPV4还是IPV6?),通常为AF_INET(表示互联网协议族(TCP/IP协议族IPV4) );
②int type:指定套接字的类型,即指定数据流套接字TCP协议(SOCK_STREAM)、数据报套接字UDP协议(SOCK_DGRAM)、SOCK_SEQPACKET;
③int protocol:通常设为0(默认类型),让系统根据我们之前设定的domain和type自动设置协议。
④返回:错误-1,成功:一个socket网络文件描述符fd。【主动连接的套接字:此时系统假设用户会对这个套接字调用connect函数,期待它主动与其它进程连接】
绑定套接字:
将套接字与本计算机上的某个端口/IP绑定
int bind(int sockfd,struct sockaddr *my_addr,int addrlen)
①int sockfd是由socket()调用返回的套接口文件描述符。
②struct sockaddr *my_addr指向数据结构sockaddr的指针
包括自己的地址、端口和IP地址的信息。
③int addrlen设置成sizeof(structsockaddr)。
④返回:成功0.失败-1
sockaddr结构用来保存socket信息:
struct sockaddr
{
unsigned short sa_family; //表示套接字的协议栈地址,对于TCP/IP可以设置为 AF_INET
char sa_data[14]; //sa_data为套接字的IP地址和端口号
};
我们计算机存储数据的方式是低位字节优先(小端模式:Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。),
而数据在网络上传输的时候是高位字节优先(大端模式:Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端),
所以一般需要先进行字节转换。其中htonl()和htons()函数是把主机字节顺序转化为网络字节顺序;ntohl()函数和ntohs()是将网络字节顺序转化为主机字节顺序。
监听网络端口请求:
将主动连接套接字转换成被动等待用户来连接。
int listen(int sockfd,int backlog)
①int sockfd:由socket函数返回,经过bind绑定的fd
②int backlog:未经过处理的连接请求队列可以容纳的最大数目。
backlog用来设置请求队列中允许的最大请求数。注意:TCP传输中为被动套接字设置了两个队列:完全建立连接的队列和未完全建立连接的队列。
③返回:成功0,失败-1
在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接。由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,用户进程通过系统调用listen来完成这件事。
这个参数涉及到一些网络的细节。在进程正理一个一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。
毫无疑问,服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内。
接收连接请求:
从完全建立连接的队列中接收一个连接。
int accept(int sockfd,void *addr,int *addrlen);
①int sockfd:监听的socket描述符
②void *addr:指向sockaddr_in变量
(存放提出连接请求服务的客户端的IP地址和端口)
③int *addrlen:sizeof(struct sockaddr_in)的指针
④返回:成功:新生成的用于服务器与客户通信的数据传输的fd
失败:-1,并设置error
面向连接的数据传输:
https://www.cnblogs.com/tianlangshu/p/6795681.html
int send(int sockfd,const void *msg,int len,unsigned int flags);
①int sockfd:发送端套接字描述符;
②const void *msg:发送数据的缓冲区;
③int len:实际要发送的数据的字节数;
④int flags:一般置0。控制选项
⑤返回:
int recv(int sockfd,const void *msg,int len,unsigned int flags);
①int sockfd:接受端套接字描述符;
②const void *msg:要接受数据的缓冲区;
③int len:缓冲的字节数;
④int flags:一般置0。控制选项
⑤返回:
关闭套接字:
int close(int sockfd);
客户端
创建套接字:
int socket(int domain,int type,int protocol);
与服务器建立连接:
int connect(int sockfd,struct sockaddr *serv_addr,int *addrlen );
①int sockfd:第一个步骤中的socket描述符
②struct sockaddr *serv_addr:指向sockaddr结构的指针
(包含了要连接的服务器端的IP地址和端口信息。)
③int *addrlen:
面向连接的数据传输:
int send(int sockfd,const void *msg,int len,unsigned int flags);
int recv(int sockfd,const void *msg,int len,unsigned int flags);
关闭套接字:
int close(int sockfd);
辅助性函数(主要是进行IP地址转换的)
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理地址由小向大增加,而数据从高位往低位放;
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
网络字节序:网络字节是只允许大段模式
有的电脑是小端模式:要转换为大端模式
转换函数:
①inet_addr会自动检测我们电脑是大端还是小端
in_addr_t inet_addr(const char *cp); //参数是一个点分十进制的IP地址字符串,转为in_addr_t,即uint32_t网络字节大端格式的二进制
②inet_aton、inet_ntoa 旧,仅限IPV4
③inet_ntop(32位→点分十进制)、inet_pton(点分十进制→32位)
新,兼容IPV4和IPV6(推荐使用)
https://www.cnblogs.com/warren-liuquan/p/3555945.html
表示IP地址相关数据结构
(1)都定义在 netinet/in.h
(2)typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型
(vi /usr/include/netinet/in.h)
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
_SOCKADDR_COMMON (sin);
in_port_t sin_port; /* Port number. /
struct in_addr sin_addr; / Internet address. /
/ Pad to size of `struct sockaddr’. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
struct sockaddr 这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充。
服务器模型
(1)【循环服务器模型和并发服务器模型】:因为对于循环服务器来说,TCP循环服务器【一次只能够处理一个客户端的请求】,处理完成后才能够接受下一个请求,所以很少使用。我们经常使用的是并发服务器模型,所谓并发,也就是说这种服务器模型在同一个时刻可以响应多个客户端的请求。它的核心思想就是每一个客户端的请求并不是由服务器进程直接处理,而是由服务器创建一个子进程来处理,这个优势可以弥补TCP循环服务器容易出现某个客户端独占服务端的缺陷。
(2)TCP并发服务器的模型如下:
(3)并发服务器模型代码示例:
https://blog.csdn.net/u011068702/article/details/61707207
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define SERV_PORT 5550 //服务器监听端口号
#define LENGTH 10 //请求队列的长度
#define SIZE 128 //缓冲区的长度
//int main(int argc, char *argv[])
int main()
{
int res;
int pth; //定义创建子进程标识符
int sockfd; //监听sockfd描述符
int clientfd; //数据传输sockfd描述符
struct sockaddr_in my_addr; //服务器(主机)IP地址和端口号信息结构体
struct sockaddr_in clientaddr; //客户端IP地址和端口号信息
unsigned int addrlen;
char buf[SIZE]; //定义缓冲区
int cnt;
① 创建tcp套接字-------------------------------------------------------------------------------------------------------
sockfd = socket(AF_INET,SOCK_STREAM,0);//参数指定IPV4 TCP
//返回值校验
if(sockfd==-1)
{
perror("创建套接字失败n");
exit(1);
}
② 配置本地服务器网络信息-------------------------------------------------------------------------
struct sockaddr_in my_addr; //sockaddr_in结构体详解
https://blog.csdn.net/renchunlin66/article/details/52351751
bzero(&my_addr, sizeof(my_addr)); // 清空
my_addr.sin_family = AF_INET; //IPV4 sin_family指代协议族,在socket编程中只能是AF_INET
my_addr.sin_port = htons(SERV_PORT); // 端口 sin_port存储端口号(使用网络字节顺序)
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); //服务器(主机)IP地址
③绑定 将套接字sockfd与本地计服务器上的某个端口my_addr绑定,进而在该端口监听服务请求------
res = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
//返回值校验
④监听,套接字主动变被动 将绑定了本地服务器端口的socket 套接字变为监听套接字(监听状态)--
res = listen(sockfd,LENGTH);//前面设定请求队列的长度LENGTH=10
//返回值校验
while(1) /*循环等待客户端的连接请求*/
{
【先用select并发IO确认sockfd可读-----https://blog.csdn.net/daaikuaichuan/article/details/83715044#font_size5selectfont_68】
addrlen = sizeof(struct sockaddr_in);//获取客户端信息结构体长度
⑤取出客户端已完成的连接 进行连接---------------------------
clientfd =accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);
//accept的第三个实参是地址 传参是指针
/*接受一个客户端连接*/
pth = fork(); //创建子进程 //也可以创建子线程进行分析处理,取代使用子进程
if(pth<0)
{
perror("子进程创建失败\n");
exit(1);
}
if(pid > 0)// 父进程
{
close(clientfd); //关闭数据传输套接字
}
if(pth == 0) //子进程,处理客户端的请求
{
//关闭不需要的套接字可节省系统资源,
//同时可避免父子进程共享这些套接字
//可能带来的不可预计的后果
close(sockfd); // 关闭监听套接字,这个套接字是从父进程继承过来
printf("客户端IP:%s\n", inet_ntoa(clientaddr.sin_addr)); //输出客户端IP地址
⑦接收&发送数据
cnt = recv (clientfd ,buf ,SIZE ,0); //接收客户端的数据
if(cnt==-1)
{
perror("接收客户端数据失败\n");
exit(1);
}
printf("收到的数据是: %s\n",buf);
close(clientfd); //关闭当前客户端的连接
exit(0); //子进程退出
}
⑧关闭连接套接字
close(clientfd); //父进程中,关闭子进程的套接字,准备接收下一个客户端的连接
}
return 0;
}
(4)多路复用 I/O 并发服务器:为了解决子进程带来的系统资源消耗问题