一、三个模型
1. B/S(Browser/Server)模型
定义:B/S模型是一种网络架构模式,其中用户通过网页浏览器(如Chrome、Firefox等)来访问和使用服务器上的应用程序或数据。
特点:
- 简化用户访问:用户无需下载和安装专门的客户端软件,只需通过浏览器即可访问系统。
- 跨平台性:由于基于浏览器,因此可以跨多个操作系统和平台使用。
- 维护和升级方便:所有的维护和升级工作都在服务器端进行,减少了客户端的维护工作。
应用场景:广泛应用于Web应用开发中,如网上银行、电子商务网站、企业信息管理系统等。
2. C/S(Client/Server)模型
定义:C/S模型是一种分布式计算架构,其中一台或多台服务器向多台客户端提供服务。客户端需要下载并安装特定的软件来与服务器进行通信。
特点:
- 性能优越:由于客户端可以处理部分逻辑和数据,因此可以减轻服务器的负担,提高系统的整体性能。
- 安全性高:服务器通常受到防火墙和安全措施的保护,数据在传输过程中也可以加密处理。
- 定制化强:客户端软件可以根据需要进行定制开发,以满足特定用户或场景的需求。
应用场景:广泛应用于各类软件系统中,如游戏客户端、邮件客户端、办公软件等。
3. P2P(Peer-to-Peer)模型
定义:P2P模型是一种网络架构模式,其中参与者(也称为节点)在网络中具有平等的地位,可以直接相互通信和交换数据,而不需要中央服务器的中介。
特点:
- 去中心化:没有中心服务器或单一控制点,节点之间可以直接通信。
- 资源利用效率高:每个节点都可以提供资源(如带宽、存储空间、计算能力等),从而提高整个系统的资源利用效率。
- 扩展性强:当有新的节点加入时,整个系统的容量也会相应增加。
应用场景:广泛应用于文件共享、内容分发、即时通信、区块链等领域。例如,BitTorrent文件共享协议和比特币区块链网络都是基于P2P模型的。
二、TCP
中文称为:传输控制协议,也称为流式套接字
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它由IETF的RFC 793定义,是互联网协议套件(Internet Protocol Suite)中的核心协议之一,与IP(Internet Protocol,互联网协议)协议一起构成了互联网的基本架构。
TCP的特点
-
面向连接:在数据传输之前,TCP双方必须先建立连接。这种连接是通过三次握手(Three-way Handshake)过程建立的,确保双方都已准备好接收数据。数据传输完成后,还需要通过四次挥手(Four-way Handshake)来关闭连接。
-
可靠性:TCP通过一系列机制来确保数据的可靠传输。这些机制包括确认应答(ACK)、超时重传、数据排序和流量控制等。TCP会为每个发送的数据包分配一个序列号,接收方在收到数据包后会发送一个确认应答(ACK)给发送方,告知其数据包已成功接收。如果发送方在一定时间内未收到确认应答,则会重新发送该数据包。
-
流量控制:TCP通过滑动窗口(Sliding Window)机制来实现流量控制,以避免发送方发送的数据量超过接收方的处理能力。接收方会在确认应答中告诉发送方自己的接收窗口大小,发送方则根据这个窗口大小来控制发送数据的速度。
-
拥塞控制:TCP还包含了一组拥塞控制算法,以应对网络拥塞的情况。当网络出现拥塞时,TCP会降低发送速率,以减轻网络的负担。
-
面向字节流:TCP将应用程序发送的数据视为无结构的字节流,而不是一组独立的数据包。TCP会根据需要将这些字节流分割成适当大小的数据包进行传输,并在接收端将它们重新组合成原始的字节流。
TCP的应用
由于TCP的可靠性和面向连接的特点,它被广泛用于需要可靠数据传输的场合。例如,HTTP(Hypertext Transfer Protocol,超文本传输协议)协议就是基于TCP的,用于在Web浏览器中传输网页数据。FTP(File Transfer Protocol,文件传输协议)和SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)等协议也是基于TCP的。
TCP的编写流程
server:socket()返回监听套接字-->bind()--->listen()-->accept()返回通信套接字-->recv()-->close()
client:socket()-->connect()-->send()-->close();
服务器端:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
1、soket
int socket(int domain, int type, int protocol);
功能:程序向内核提出创建一个基于内存的套接字描述符
参数:domain 地址族,PF_INET == AF_INET ==>互联网程序
PF_UNIX == AF_UNIX ==>单机程序
type 套接字类型:
SOCK_STREAM 流式套接字 ===》TCP
SOCK_DGRAM 用户数据报套接字===>UDP
SOCK_RAW 原始套接字 ===》IP
protocol 协议 ==》0 表示自动适应应用层协议。
返回值:成功 返回申请的套接字id
失败 -1;
2.bind
int bind(int sockfd, struct sockaddr *my_addr,
socklen_t addrlen);
功能:如果该函数在服务器端调用,则表示将参数1相关
的文件描述符文件与参数2 指定的接口地址关联,
用于从该接口接受数据。
如果该函数在客户端调用,则表示要将数据从
参数1所在的描述符中取出并从参数2所在的接口
设备上发送出去。
注意:如果是客户端,则该函数可以省略,由默认
接口发送数据。
参数:sockfd 之前通过socket函数创建的文件描述符,套接字id
my_addr 是物理接口的结构体指针。表示该接口的信息。
struct sockaddr 通用地址结构
{
u_short sa_family; 地址族
char sa_data[14]; 地址信息
};
转换成网络地址结构如下:
struct _sockaddr_in ///网络地址结构
{
u_short sin_family; 地址族
u_short sin_port; ///地址端口
struct in_addr sin_addr; ///地址IP
char sin_zero[8]; 占位
};
struct in_addr
{
in_addr_t s_addr;
}
socklen_t addrlen: 参数2 的长度。
返回值:成功 0
失败 -1;
3、listen
3、 int listen(int sockfd, int backlog);
功能:在参数1所在的套接字id上监听等待链接。
参数:sockfd 套接字id
backlog 允许同一时刻链接的个数。不是可链接的个数
返回值:成功 0
失败 -1;
4、accept
int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
功能:从已经监听到的队列中取出有效的客户端链接并
接入到当前程序。
参数:sockfd 套接字id
addr 如果该值为NULL ,表示不论客户端是谁都接入。
如果要获取客户端信息,则事先定义变量
并传入变量地址,函数执行完毕将会将客户端
信息存储到该变量中。
addrlen: 参数2的长度,如果参数2为NULL,则该值
也为NULL;
如果参数不是NULL,&len;
一定要写成len = sizeof(struct sockaddr);
返回值:成功 返回一个用于通信的新套接字id;
从该代码之后所有通信都基于该id
失败 -1;
5、接受函数/发送函数:recv函数
read()/write () ///通用文件读写,可以操作套接字。
recv(,0) /send(,0) ///TCP 常用套机字读写
recvfrom()/sendto() ///UDP 常用套接字读写
ssize_t recv(int sockfd, void *buf, size_t len,
int flags);
功能:从指定的sockfd套接字中以flags方式获取长度
为len字节的数据到指定的buff内存中。
参数:sockfd
如果服务器则是accept的返回值的新fd
如果客户端则是socket的返回值旧fd
buff 用来存储数据的本地内存,一般是数组或者
动态内存。
len 要获取的数据长度
flags 获取数据的方式,0 表示阻塞接受。
返回值:成功 表示接受的数据长度,一般小于等于len
失败 -1;
6、close() ===>关闭指定的套接字id;
客户端:
1、connect
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:该函数固定有客户端使用,表示从当前主机向目标
主机发起链接请求。
参数:sockfd 本地socket创建的套接子id
addr 远程目标主机的地址信息。
addrlen: 参数2的长度。
返回值:成功 0
失败 -1;
2、send
int send(int sockfd, const void *msg,
size_t len, int flags);
功能:从msg所在的内存中获取长度为len的数据以flags
方式写入到sockfd对应的套接字中。
参数:sockfd:
如果是服务器则是accept的返回值新fd
如果是客户端则是sockfd的返回值旧fd
msg 要发送的消息
len 要发送的消息长度
flags 消息的发送方式。
返回值:成功 发送的字符长度
失败 -1;
用tcp进行聊天:
服务端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include <signal.h>
typedef struct sockaddr* (SA);
int main(int argc, char *argv[])
{
//监听套接字
int listfd = socket(AF_INET,SOCK_STREAM,0 );
if(-1 ==listfd)
{
perror("socket");
exit(1);
}
struct sockaddr_in ser,cli;
bzero(&ser,sizeof(ser));
bzero(&cli,sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr =inet_addr("127.0.0.1");
int on = 1;
setsockopt(listfd, SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
int ret = bind(listfd,(SA)&ser,sizeof(ser));
if(-1 ==ret)
{
perror("bind");
exit(1);
}
//建立连接的排队数
listen(listfd,3);
socklen_t len = sizeof(cli);
//通讯套接字
int conn = accept(listfd,(SA)&cli,&len);
if(-1 == conn)
{
perror("accept");
exit(1);
}
pid_t pid = fork();
if(pid>0)
{
while(1)
{
char buf[128]={0};
printf("to cli:");
fgets(buf,sizeof(buf),stdin);
int se_ret = send(conn,buf,strlen(buf),0);
if(0 == strcmp("#quit\n",buf)
||se_ret<0)
{
kill(pid,2);
exit(0);
}
}
}
else if(0 == pid)
{
while(1)
{
char buf[128]={0};
int rd_ret = recv(conn,buf,sizeof(buf),0);
if(0 == strcmp(buf,"#quit\n")
|| rd_ret<=0)
{
kill(getppid(),2);
exit(0);
}
printf("cli:%s",buf);
fflush(stdout);
}
}
else
{
perror("fork");
exit(1);
}
close(listfd);
close(conn);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include <pthread.h>
typedef struct sockaddr* (SA);
void* th1(void* arg)
{
int sockfd = *(int*)arg;
while(1)
{
char buf[128]={0};
printf("to ser:");
fgets(buf,sizeof(buf),stdin);
int se_ret = send(sockfd,buf,strlen(buf),0);
if(0==strcmp(buf,"#quit\n")
|| se_ret<0)
{
exit(0);
}
}
return NULL;
}
void* th2(void* arg)
{
int sockfd = *(int*)arg;
while(1)
{
char buf[128]={0};
int rd_ret = recv(sockfd,buf,sizeof(buf),0);
if(0 == strcmp(buf,"#quit\n")
||rd_ret<=0)
{
exit(0);
}
printf("ser:%s",buf);
fflush(stdout);
}
return NULL;
}
int main(int argc, char *argv[])
{
int conn= socket(AF_INET,SOCK_STREAM,0);
if(-1 == conn)
{
perror("socket");
exit(1);
}
struct sockaddr_in ser;
bzero(&ser,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr =inet_addr("127.0.0.1");
int ret = connect(conn,(SA)&ser,sizeof(ser));
if(-1 == ret)
{
perror("connect");
exit(1);
}
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,th1,&conn);
pthread_create(&tid2,NULL,th2,&conn);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
close(conn);
return 0;
}
TCP四次挥手非正常断开
非正常断开:会有冷却时间,不到2分钟(命令行显示bind: Address already in use)
可以使用一下函数解决:函数要设置在bind函数之前
int on = 1;
setsockopt(listfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
-
int on = 1;
:定义了一个整型变量on
并将其值设置为1。这个变量用于指示SO_REUSEADDR
选项的启用状态。在这个上下文中,1表示启用地址重用。 SO_REUSEADDR
:允许本地地址和端口的重用。这对于服务器程序非常有用,因为它们通常需要绑定到一个固定的端口上。
这行代码的目的是为了允许listfd
所引用的套接字在相同的地址和端口上重用之前可能已存在的套接字。这在开发需要频繁重启的服务器程序时非常有用,因为它可以避免因等待TIME_WAIT状态结束而导致的延迟。