前言
最近新换了家实习单位,趁着leader去交通局开会,偷个闲整理一下有关于socket通信方面的知识。
socket
socket可以将其理解为一种“中间件”,即TCP/IP协议族与应用层通信的中间软件,它是一组接口。恰巧最近在看自动驾驶中的“中间件”,我们可以将其理解:我们是做“炸串”的寡头、其中肉串的好坏对我们至关重要,我们可能不需要去自己亲自挑选肉串,可以让某些代理机构去测评并提供新鲜的“肉串”至于肉串是谁家做的,我们不关心,我们只关注肉的质量。其中代理机构类似与“中间件”,将我们与提供肉串的厂商分隔开来,每个部分只专注于其本身擅长的事。就像挑选“肉串一样”,TCP/IP协议很复杂,socket将其封装提供给用户使用。
socket起源于Unix,而Unix/Unix基本哲学就是“一切皆为文件”,我们知道文件的操作模式为:
“open->write/read->close”模式,我们将socke可以理解为对数据流的一种读写操作,socke帮我们封装好了对应的函数,后面我们将介绍。
TCP/IP及UDP简介
TCP/IP(Transmission Control Protocol/Internet Protocol)传输控制协议/网间协议,是一个工业标准的协议集,他是为广域网(WANs)设计的。UDP(User Data Protocol)用户数据协议与TCP相对应的协议。
TCP/IP协议簇包括应用层、传输层、网络层、链路层(数据链路层、物理层)socket处于的位置为应用层和运输层之间。
TCP/IP 三次握手建立连接
TCP建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
客户端向服务器发送一个SYN J服务器向客户端响应响应一个SYN K 并对SYN K 进行确认 ACK + 1客户端再向服务器发一个确认ACK K+1
只有进行完三次握手之后才可以建立连接。
从图中可以看出,当客户端调用connect的时候,触发了连接请求,向服务器发送SYN J包 这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。
socket 中TCP的四次握手释放
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送。
(2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。
(3) 服务器关闭客户端的连接,发送一个FIN给客户端。
(4) 客户端发回ACK报文确认,并将确认序号设置为收到序号加1。
socket通信流程
socket是"open->write/read->close"模式实现的,交互流程如下图所示:
实例代码
示例一:server只能接收不能发送
示例一:server端
/**服务器只能够接收客户端发来的消息**/#include <sys/types.h>#include <sys/socket.h>#include <stdio.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#include <fcntl.h>#include <sys/shm.h>#include <thread>#include <iostream>using namespace std;#define PORT 7000#define QUEUE 20 //连接请求队列int conn;/***1.创建一个socket,用函数socket() line312.设置socket属性,用函数setsockopt();可选3.绑定IP地址,端口等信息到socket上,用函数bind();line384.开启监听,用函数listen();line445.接收客户端上来的连接,用accept()6.收发数据,用函数send()和recv(),7.关闭网络连接;8.关闭监听***/void thread_task(){}int main(){cout << "AF_INET : %d " <<AF_INET << endl;printf("%d /n", SOCK_STREAM);int ss = socket(AF_INET,SOCK_STREAM,0);//若成成功则返回一个sockefd(套接子描述符)printf("%d/n",ss);struct sockaddr_in server_sockaddr;//一般存储地址和端口用于信息的存储和显示server_sockaddr.sin_family = AF_INET;server_sockaddr.sin_port = htons(PORT);//将一个无符号短整型转换为网络字节序,即大端模式server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);//将主机的无符号长整型转换成网络字节顺序if(bind(ss, (struct sockaddr*) & server_sockaddr, sizeof(server_sockaddr))== -1){perror("bind");exit(1);}if(listen(ss,QUEUE) == -1){perror("listen");exit(1);}char buffer[1024];//创建另外一个线程//std::thread t(thread_task);//t.join();//char buf[1024];//主线程while (1){memset(buffer, 0, sizeof(buffer));int len = recv(conn, buffer, sizeof(buffer), 0);//从TCP连接的另一端接受数据/*该函数的第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;第三个参数指明buf的长度;第四个参数一般置0*/if(strcmp(buffer,"exit\n") == 0){break;}printf("%s",buffer);//必须要有返回数据,这样才算一个完整的请求send(conn, buffer, len, 0);//向TCP连接的另一端发送数据}close(conn);//因为accept函数链接成功后还会生成一个新的套接字描述,结束之后也需要关闭close(ss);//关闭套接字描述符return 0;}
示例一:客户端
/*局域网TCP客户端*/#include <sys/types.h>#include <sys/socket.h>#include <stdio.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#include <fcntl.h>#include <sys/shm.h>#include <iostream>#define MYPORT 7000#define BUFFER_SIZE 1024/*1.创建一个socket,用函数socket();2.设置socket属性,用函数setsockopt();*可选3.绑定IP地址、端口等信息到socket上,用函数bind();4.循环接收数据,用函数recvfrom();5.关闭网络连接;*/int main(){//定义sockfdint sock_cli = socket(AF_INET,SOCK_STREAM,0);//定义sockaddr_instruct sockaddr_in servaddr;memset(&servaddr, 0 , sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(MYPORT);//服务器端口servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服务器ip,inet_addr用于IPv4的IP转换(十进制转换为二进制)//127.0.0.1是本地预留地址//连接服务器,成功返回0,错误返回-1if(connect(sock_cli, (struct sockaddr*)&servaddr, sizeof(servaddr)) <0){perror("connect");exit(1);}char sendbuf[BUFFER_SIZE];char recvbuf[BUFFER_SIZE];while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) //从stream流中读取size个字符到所指向的内存区域{/*每次读取一行,读取的数据保存在buf指向的字符串数组中,成功则返回地一个参数的buf:*/send(sock_cli, sendbuf, strlen(sendbuf), 0);//发送std::cout << " judge send is : " << send <<std::endl;if(strcmp(sendbuf, "exit\n") == 0){break;}recv(sock_cli,recvbuf, sizeof(recvbuf), 0);fputs(recvbuf, stdout);//把字符串写入到指定的流,但是不包括空字符memset(sendbuf, 0 , sizeof(sendbuf));memset(recvbuf, 0 , sizeof(recvbuf));}close(sock_cli);return 0;/*在TCP三次握手完成之后会进入等待连接队列,等待服务端调用accept与之建立链接,这时候是sever>端调用accept跟客户端建立通信,客户端并不需要调用accept,因为很多个客户端要跟服务端建立连接,这时候服务端就会有一个>队列,对已经经过三次握手的才可以建立连接(类似与缓存消息),这是由服务端来确认的,客户端并不知道什么时候服务端才能>跟它建立连接,在服务端没有调用accept与之连接或者还未排队到它,只能是一直等待,直到服务端准备好了才能跟客户端建立连接>,所以主动权在服务端*/}
运行效果
先启动服务器,然后客户端发送“hello”,服务器收到信息之后,在返回给客户端“hello”。
参考博客
C++ socket通信详解
Linux 下的C++ socket 编程实例