通信篇Socket通信机制
【通信篇】【Socket通信机制】
系列文章
1.简介
Socket通信机制是一种计算机网络通信协议,它是基于TCP/IP协议栈的应用层协议,提供了一组API(应用程序接口),允许不同的进程在不同计算机之间进行通信。socket通信一般涉及客户端和服务端两个socket。服务器端首先初始化Socket,然后与端口绑定,并对该端口进行监听。当客户端连接成功后,客户端会发送数据请求,服务器端接收并处理这些请求,然后将回应数据发送给客户端。客户端读取数据后,连接关闭。
socket本身不是协议,是api接口,通过socket,我们可以使用Tcp/ip协议。
图片来自网络(如有侵权,联系我,会删除)
socket流程图
图片来自网络(如有侵权,联系我,会删除)
1.1 Tcp协议
Tcp协议:TCP(Transmission Control Protocol)协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。它旨在适应支持多网络应用的分层协议层次结构,确保数据在传输过程中的可靠性、完整性和顺序性。
1.1.1 Tcp协议的工作原理
1.三次握手建立连接:
第一步:客户端发送一个请求(SYN包)到服务器。
第二步:服务器收到请求后,发送确认和自己连接的请求(SYN/ACK包)给客户端,
第三步:客户端再发送确认(ACK包)连接成功。
socket中TCP协议三次握手如下:
2.数据传输:
发送方将数据划分成小块(称为报文段),并添加头部和校验等信息,然后通过TCP协议将这些报文段发送给接收方。接收方收到报文段后,校验数据的完整性,并把它们重新组装成完整的数据流。
3.四次挥手关闭连接:
第一次挥手:客户端发送断开连接请求,客户端发送一个包含FIN标志的报文,表示客户端已经完成了数据的发送,并请求关闭连接,等待服务端的确认。
第二次挥手:服务端受到客户端发送的FIN标志的报文,发送一个确认(ACK)报文,服务器此时可能还有剩余的数据要发送给客户端。因此服务端继续发送数据,并进入close-wait(等待关闭)状态。而客户端受到确认后,tcp会处于半关闭状态,即:客户端不能发送数据,但是还可以接受数据。
第三次挥手:当服务端完成数据发送以后,服务端发送一个FIN报文给客户端,即服务端向客户端发送断开连接请求。此时服务器进入LASK_ACK(最后确认状态),等待客户端的确认。
第四次挥手:客户端受到服务端的FIN报文后,再次发送一个确认(ACK)报文。然后客户端进入TIME_WAIT(时间等待状态),为了防止网络堵塞,确保服务端已经受到确认报文。然后服务端受到ACK应答报文后,服务端就进入了close状态。此时客户端处于TIME_WAIT状态,此时tcp仍未释放,需要等待2MSL后,客户端才进入CLOSE状态。
MSL:代表报文在网络上存在的最长时间,如果超过此时间,报文会被丢弃,如果是TCP协议,路由器会触发重新发送的机制。
1.2 IP协议
IP协议,即Internet Protocol,是TCP/IP体系中的网络层协议。
2.socket如何用?
2.1 服务端
2.1.1 步骤
第一步:创建一个socket
1.int socket(int domain, int type, int protocol);
//创建一个套接字
//domain:协议族,AF_INET、AF_INET6、AF_UNIX,AF_INET代表是ipV4,AF_INET6代表是ipv6
//type:socket类型。SOCK_STREAM、SOCK_DGRAM
//protocol:协议,0 使用type对应的默认协议
//返回值:
//成功: 新套接字所对应文件描述符
//失败: -1 errno
第二步:初始化serviceaddr,然后bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//给socket绑定一个地址结构 (IP+port)
//sockfd:socket 函数返回值
//struct sockaddr_in addr;
//addr.sin_family = AF_INET;
//addr.sin_port = htons(8888);
//addr.sin_addr.s_addr = htonl(INADDR_ANY);
//addr: 传入参数(struct sockaddr *)&addr
//addrlen: sizeof(addr) 地址结构的大小。
//返回值:
//成功:0
//失败:-1 errno
第三步:通过listen监听socket,用于监听client发送的连接请求。
int listen(int sockfd, int backlog);
//设置同时与服务器建立连接的上限数(同时进行3次握手的客户端数量)
//sockfd:socket 函数返回值
//backlog:上限数值,最大值 128
//返回值:
//成功:0
//失败:-1 errno
第四步:通过accept接受客户端的请求。阻塞服务端,等待客户端的连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
//sockfd:socket 函数返回值
//addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
//socklen_t clit_addr_len = sizeof(addr);
//addrlen:传入传出参数,&clit_addr_len
//入:addr的大小。 出:客户端addr实际大小。
//返回值:
//成功:能与客户端进行数据通信的 socket 对应的文件描述。
//失败: -1 , errno
第五步:读取客户端发送的数据。
ssize_t read(int fd, void *buf, size_t count);
//fd:是一个文件描述符,在socket编程中就是socket的文件描述符。
//buf:是一个指向缓冲区的指针,数据将从socket中读取到这个缓冲区中。
//count:指定了最多读取的字节数。
//返回值:
//-1: 表示读取过程中出现了错误
// 0: 如果read函数返回0,这通常意味着对端已经关闭了连接,或者是一个非阻塞的socket在没有数据可读时
// 返回的结果。对于TCP连接,这通常意味着对端已经发送了一个FIN包来关闭连接,并且本地socket也接收
// 到了这个FIN包。
//正整数:如果read函数成功读取了一些数据,它将返回读取到的字节数。
第六步:write向客户端发送数据。
ssize_t write(int fd, const void *buf, size_t count);
//fd:是一个文件描述符,在socket编程中即socket的文件描述符。
//buf:是一个指向要发送数据的缓冲区的指针。
//count:指定了要写入的数据的字节数。
//返回值:
//正数:write函数成功地将数据写入socket的发送缓冲区,它将返回写入的字节数
//-1 : 写入数据时遇到错误
第六步 :通过close关闭socket
int close(int fd);
//fd:fd是一个整数类型的文件描述符,它标识了需要关闭的socket
//返回值:
//0:代表close函数调用成功
//-1:代表调用失败
2.1.2 服务端代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include <unistd.h>
#include <ctype.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include <arpa/inet.h>
#define DEFAULT_PORT 10997 //默认端口
#define MAXLINE 4096
int main(int argc, char** argv)
{
//第一步
int servicesocket_fd;
if(servicesocket_fd = socket(AF_INET,SOCK_STREAM,0) == -1)//创建socket
{
printf("service create socket error\n");
}
//第二步
//1.初始化serviceaddr,然后bind
struct sockaddr_in serviceaddr;
memset(&serviceaddr,0,sizeof(serviceaddr));
serviceaddr.sin_family = AF_INET;//设置协议族
//serviceaddr.sin_addr.s_addr = inet_addr("172.17.0.1");//设置ip地址。inet_addr作用是用于ipv4的转换。
serviceaddr.sin_addr.s_addr = htonl(INADDR_ANY);//获取本机的有效ip地址
serviceaddr.sin_port = htons(DEFAULT_PORT);//设置端口号,htons的作用是将整形变量从主机字节顺序转化为网络字节顺序
if(bind(servicesocket_fd,(struct sockaddr*)&serviceaddr,sizeof(serviceaddr))== -1){
printf("socket bind fail\n");
return 0;
}
//第三步
if(listen(servicesocket_fd,4)== -1){
printf("socket listen fail\n");
return 0;
}
struct sockaddr_in clientaddr;
socklen_t clientaddrsize = sizeof(clientaddr);
int clientsocket_fd =accept(servicesocket_fd,(struct sockaddr*)&clientaddr,&clientaddrsize);
if(clientsocket_fd ==-1){
printf("accept fail\n");
return 0;
}
char readbuf[1024];
char writebuf[1024] = {'s','e','w','r'};
while(1){//死循环的读取客户端发送的数据
int n = read(clientsocket_fd,readbuf,sizeof(readbuf));
if(n>0){
printf("readbuf =%s",readbuf);
write(clientsocket_fd,writebuf,sizeof(writebuf));
continue;
}
}
close(servicesocket_fd);
close(clientsocket_fd);
return 0;
}
2.2 客户端
2.2.1 步骤
第一步:创建客户端socket
int socket(int domain, int type, int protocol);
// 创建一个套接字
// domain:协议族,AF_INET、AF_INET6、AF_UNIX,AF_INET代表是ipV4,AF_INET6代表是ipv6
// type:socket类型。SOCK_STREAM、SOCK_DGRAM
// protocol:协议,0 使用type对应的默认协议
// 返回值:
// 成功: 新套接字所对应文件描述符
// 失败: -1 errno
第二步:连接服务端
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 使用现有的 socket 与服务器socket建立连接
// sockfd: clientsocket 函数返回值
// struct sockaddr_in srv_addr; // 服务器地址结构
// srv_addr.sin_family = AF_INET;
// srv_addr.sin_port = 9527 // 跟服务器bind时设定的 port 完全一致。
// inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
// addr:传入参数,服务器的地址结构
// addrlen:服务器的地址结构的大小
// 返回值:
// 成功:0
// 失败:-1 errno
// 如果不使用bind绑定客户端地址结构, 采用"隐式绑定"。
第三步:通过write向服务端发送数据
ssize_t write(int fd, const void *buf, size_t count);
//fd:是一个文件描述符,在socket编程中即socket的文件描述符。
//buf:是一个指向要发送数据的缓冲区的指针。
//count:指定了要写入的数据的字节数。
//返回值:
//正数:write函数成功地将数据写入socket的发送缓冲区,它将返回写入的字节数
//-1 : 写入数据时遇到错误
第四步:通过read读取来自服务端的数据
ssize_t read(int fd, void *buf, size_t count);
//fd:是一个文件描述符,在socket编程中就是socket的文件描述符。
//buf:是一个指向缓冲区的指针,数据将从socket中读取到这个缓冲区中。
//count:指定了最多读取的字节数。
//返回值:
//-1: 表示读取过程中出现了错误
// 0: 如果read函数返回0,这通常意味着对端已经关闭了连接,或者是一个非阻塞的socket在没有数据可读时
// 返回的结果。对于TCP连接,这通常意味着对端已经发送了一个FIN包来关闭连接,并且本地socket也接收
// 到了这个FIN包。
//正整数:如果read函数成功读取了一些数据,它将返回读取到的字节数。
第五步:关闭socket,释放资源
int close(int fd);
//fd:fd是一个整数类型的文件描述符,它标识了需要关闭的socket
//返回值:
//0:代表close函数调用成功
//-1:代表调用失败
2.2.2 客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define DEFAULT_PORT 10997
int main(int argc, char** argv)
{
int clientsocket_fd;
if (clientsocket_fd = socket(AF_INET, SOCK_STREAM, 0) == -1)
{
printf("create clientsocket_fd fail\n");
return 0;
}
// 第二步:
// 1.初始化serviceaddr
struct sockaddr_in serviceaddr;
serviceaddr.sin_family = AF_INET; // 设置协议族
serviceaddr.sin_port = htons(DEFAULT_PORT); // 设置端口号,htons的作用是将整形变量从主机字节顺序转化为网络字节顺序
//serviceaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置ip地址。inet_addr作用是用于ipv4的转换。
serviceaddr.sin_addr.s_addr = htonl(INADDR_ANY); //获取本机的有效ip地址
if (connect(clientsocket_fd, (struct sockaddr *)&serviceaddr, sizeof(serviceaddr)) == -1)
{
printf("client connect fail\n");
return 0;
}
char clientwritebuf[1024] = {'c','l','w','t','\n'};
int wr =write(clientsocket_fd,clientwritebuf,sizeof(clientwritebuf));
char clientreadbuf[1024];
while(1){
int cre = read(clientsocket_fd,clientreadbuf,sizeof(clientreadbuf));
if (cre>0){
printf("clientreadbuf = %s\n",clientreadbuf);
break;
}
}
close(clientsocket_fd);
return 0;
}