系列文章目录
文章目录
前言
一、网络数据的五元组信息
1.理解源IP地址和目的IP地址
- 在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址:
- 源IP地址:表示该条信息来源于哪个机器。
- 目的IP地址:表示该条信息去往于哪个进程。
2.理解 “端口号” 和 “进程ID”
- 端口号(port)是传输层协议的内容:
- 端口号是一个2字节16位的整数。
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
- 一个端口号只能被一个进程占用。
- 一个进程可以绑定多个端口号。
- pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程。
3.理解源端口号和目的端口号
- 传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”:
- 源端口号:表示该条信息来源于哪个进程。
- 目的端口号:表示该条信息去往于哪个机器。
以寄快递为例子:
4.理解TCP协议
- 协议:两台机器传输时用哪种协议。
- TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题。
- 传输层协议。
- 有连接:双方在发送网络数据之前必须建立连接,在进行发送。
- 可靠传输:保证数据是可靠并且有序的到达对端,例如发送123、456时123数据先到达,456数据后到达,但是有时可以456数据先到达传输层,但会阻塞等待先等前面的数据就是123先到达。
- 面向字节流:TCP发送数据的单位是以字节为单位,并且数据没有明显的边界例如:123456数据不会分开。
假设应用层要想传输层传入“hello”,当hello传入传输层还尾传入网络层时,应用层又想向传输层传入“world”,此时是不能传输的,只有等“hello”从传输层传入网络层,“world”才能从应用层传入传输层:
5.理解UDP协议
- 此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论。
- 传输层协议。
- 无连接:双方在发送网络数据之前不需要建立连接,直接发送,客服端不用管服务端是否在线。
- 可靠传输:UDP并不会保证数据有序的到达对端
- 面向字节流:UDP不管向应用层还是网络层传递数据都是整条数据
假设A机器的应用层先向传输层传入一个“aaa”,再向传输层传入一个“bbb”,到待对端机器的传输层不会区分,是不是一次传过来的:
、
二、主机字节序<===>网络字节序
- 我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
- 网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
- 网络数据需要进行转发之前:由主机字节序转换成为网络字节序。
- 网络数据接收之前:由网络字节序转换成为主机字节序。
- 【问题】为什么网络数据需要进行转化成为网络字节序?
- 网络规定采用大端字节序作为网络字节序。
- 路由设备或者交换机需要对网络数据进行分用到网络层面,以获取到“目的IP地址”,而这些设备在进行分用的时候默认是按照网络字节序进行分用的。
- 主机字节序转换为网络字节序(host to network)
- 2个字节 uint16_t htons(uint16_t hostshort)。
- 4个字节 uint32_t htonl(uint32_t hostlong)。
- 网络字节序转换为主机字节序( to network)
- 2个字节 uint16_t ntohs(uint16_t netshort);
- 4个字节 uint32_t ntohl(uint32_t netlong);
三、点分十进制IP<===>uint32_t
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;字符串转in_addr的函数。
- 点分十进制IP转换成为uint32_t
- in_addr_t inet_addr(const char * cp);
- 将字符串的点分十进制IP地址转换为uint32_t
- 将uint32_t从主机字节序转换成为网络字节序。
- uint32_t转换成为点分十进制IP
- char * inet_ntoa(struct in_addr in);
- 将网络字节序uint32_t的整数转换成为主机字节序。
- 将uint32_t转换成为点分十进制的字符串。
四、UDP的socket编程(流程&接口)
1.UDP的socket编程流程
- cs模型(客户端服务端):client-server。
- bs模型:浏览器-服务器。
1.socket常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr*addr,socklen_t addrlen);
2.socketaddr结构的分类
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。
- IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
- socket API可以都用struct sockaddr * 类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
3.socketaddr结构
4.socketaddr_in结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。
5.in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
2.UDP的socket编程接口
1.创建套接字socket接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
1 #include<iostream>
2 #include<sys/types.h>
3 #include<sys/socket.h>
4 #include<unistd.h>
5
6 using namespace std;
7 int main()
8 {
9 int SockFd=socket(AF_INET,SOCK_STREAM,0);
10 if(SockFd<0)
11 {
12 cout<<"套接字创建失败!"<<endl;
13 }
14 cout<<"SockFd:"<<SockFd<<endl;
15 while(1)
16 {
17 sleep(1);
18 }
19 return 0;
20 }
2.绑定端口号bind接口
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
- sockfd:socket函数返回的套接字描述符;将创建出来的套接字和网卡,端口好进行绑定
- addr:地址信息结构
- addr的类型是struct sockaddr ,struct sockaddr 是一个通用地址信息结构,如下图所示:
假设,定义一个int fun(void * x)参数可以接收任何类型数据的函数,使用时就需要强转 char* p = “abc”; fun((void*)lp);而如上结构体的作用相当于此例中的参数,因为bind函数可能绑定 ipv4(uint32_t) / ipv6(uint128_t) / 本地域套接字 等不同类型的协议,所以绑定不同版本的IP地址需要提供不同的绑定函数,而此做法非常的麻烦,所以将协议的数据结构定义为一个通用的,要使用某一具体的协议,只需传入具体的协议对应的数据结构并强转即可。
如下图所示,我们可以在 vim /usr/include/netinet/in.h 路径下查看ipv4协议使用的结构体:- addrlen:地址信息结构的长度(告诉网络协议栈最多能解析多少个字节)
1 #include<iostream>
2 #include<sys/types.h>
3 #include<sys/socket.h>
4 #include<unistd.h>
5 #include<netinet/in.h>
6 #include<arpa/inet.h>
7 using namespace std;
8
9 int main()
10 {
11 int SockFd=socket(AF_INET,SOCK_DGRAM,0);
12 if(SockFd<0)
13 {
14 cout<<"套接字创建失败!"<<endl;
15 }
16 cout<<"SockFd:"<<SockFd<<endl;
17
18 struct sockaddr_in addr;
19
20 addr.sin_port=htons(20000);
21 addr.sin_family=AF_INET;
22 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
23 int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
24 if(ret<0)
25 {
26 cout<<"绑定失败!"<<endl;
27 return 0;
28 }
29 while(1)
30 {
31 sleep(1);
32 }
33 return 0;
34 }
3.UDP发送接口sendto
ssize_t sendto(int sockfd, const void * buf, size_t len, int flags,const struct sockaddr * dest_addr, socklen_t addrlen);
- sockfd:套接字描述符
- buf:要发送的数据
- len:发送数据的长度
- flags:0 阻塞发送
- dest_addr:目标主机的地址信息结构(IP,port)
- addrlen:目标主机地址信息结构的长度
- 返回值:
成功返回具体发送的字节数量,失败返回-1
4.UDP接收接口recvform
ssize_t recvfrom(int sockfd, void * buf, size_t len, int flags,struct sockaddr * src_addr, socklen_t * addrlen);
- sockfd:套接字描述符
- buf:将数据接收到buf当中
- len:buf的最大接收能力
- flags:0阻塞接收
- src_addr:这个数据来源的主机的地址信息结构(IP,port)---->由recvfrom()函数填充
- addrlen:输入输出型参数
输入:在接收之前准备的对端地址信息结构的长度
输出:实际接收回来的地址信息长度
5.UDP关闭接口close
close(int sockfd);
3.客户端为什么不推荐绑定地址信息
本质上是不想让客户端程序将端口写死,即不想让客户端在启动的时候,都是绑定一个端口的(一个端口只能被一个进程所绑定)。
eg:客户端A绑定了端口,本机在启动客户端B的时候就会绑定失败
当客户端没有主动的绑定端口,UDP客户端在调用sendto的时候,会自动绑定一个空闲的端口(操作系统分配一个空闲的端口)。
五、UDP的socket编程代码
1.客户端
客户端只需创建套接字,向服务端发送请求,接收服务端的回复即可。
1 #include<iostream>
2 #include<stdio.h>
3 #include<sys/types.h>
4 #include<sys/socket.h>
5 #include<unistd.h>
6 #include<netinet/in.h>
7 #include<arpa/inet.h>
8 #include<string.h>
9 #include<stdlib.h>
10 using namespace std;
11
12 int main()
13 {
14 int SockFd=socket(AF_INET,SOCK_DGRAM,0);
15 if(SockFd<0)
16 {
17 cout<<"套接字创建失败!"<<endl;
18 }
19 cout<<"SockFd:"<<SockFd<<endl;
20
21 /* struct sockaddr_in addr;
22
23 addr.sin_port=htons(20000);
24 addr.sin_family=AF_INET;
25 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
26 int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
27 if(ret<0)
28 {
29 cout<<"绑定失败!"<<endl;
30 return 0;
31 }*/
32 while(1)
33 {
34 char buf[1024]="i am client!";
35 struct sockaddr_in dest_addr;
36 dest_addr.sin_family=AF_INET;
37 dest_addr.sin_port=htons(20000);
38 dest_addr.sin_addr.s_addr=inet_addr("1.14.165.138");
39
40 sendto(SockFd,buf,strlen(buf),0,(struct sockaddr*)&dest_addr,sizeof(dest_addr));
41
42 memset(buf,'\0',sizeof(buf));
43
44 struct sockaddr_in peer_addr;
45 socklen_t len=sizeof(peer_addr);
46
47 ssize_t rece_size=recvfrom(SockFd,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer_addr,&len);
48 if(rece_size<0)
49 {
50 continue;
51 }
52 cout<<"recv msg:"<<buf<<" from "<<inet_ntoa(peer_addr.sin_addr)<<" "<<ntohs(peer_addr.sin_port)<<endl;
53 sleep(1);
54 }
55 close(SockFd);
56 return 0;
57 }
2.服务端
服务端只需创建套接字,绑定端口,接收客户端的请求,回复客户端信息即可。
1 #include<iostream>
2 #include<stdio.h>
3 #include<sys/types.h>
4 #include<sys/socket.h>
5 #include<unistd.h>
6 #include<netinet/in.h>
7 #include<arpa/inet.h>
8 #include<string.h>
9 #include<stdlib.h>
10 using namespace std;
11
12 int main()
13 {
14 int SockFd=socket(AF_INET,SOCK_DGRAM,0);
15 if(SockFd<0)
16 {
17 cout<<"套接字创建失败!"<<endl;
18 }
19 cout<<"SockFd:"<<SockFd<<endl;
20
21 struct sockaddr_in addr;
22
23 addr.sin_port=htons(20000);
24 addr.sin_family=AF_INET;
25 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
26 int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
27 if(ret<0)
28 {
29 cout<<"绑定失败!"<<endl;
30 return 0;
31 }
32 while(1)
33 {
34 char buf[1024]={0};
35 struct sockaddr_in peer_addr;
36 socklen_t len=sizeof(peer_addr);
37 ssize_t rece_size=recvfrom(SockFd,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer_addr,&len);
38 if(rece_size<0)
39 {
40 continue;
41 }
42 cout<<"recv msg:"<<buf<<" from "<<inet_ntoa(peer_addr.sin_addr)<<" "<<ntohs(peer_addr.sin_port)<<endl;
43
44 memset(buf,'\0',sizeof(buf));
45 sprintf(buf,"welcome client %s:%d\n",inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port));
46 sendto(SockFd,buf,strlen(buf),0,(struct sockaddr*)&peer_addr,sizeof(peer_addr));
47 }
48 close(SockFd);
49 return 0;
50 }
3.查看端口的使用情况:netstat -anp | grep [端口号]
六、TCP的socket编程(流程&接口)
1.TCP的socket编程流程
2.TCP的socket编程接口
创建套接字接口socket(),绑定端口bind(),关闭套接字接口close(),的使用和UDP套接字编程中的使用是一样的,下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。
1.服务端创建套接字socket接口
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符。
- 应用程序可以像读写文件一样用read/write在网络上收发数据。
- 如果socket()调用出错则返回-1。
- 对于IPv4, family参数指定为AF_INET。
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议。
- protocol参数的介绍从略,指定为0即可。
1 #include<unistd.h>
2 #include<iostream>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/socket.h>
6 #include<arpa/inet.h>
7 #include<error.h>
8 #include<stdio.h>
9 using namespace std;
10
11 int main()
12 {
13 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
14
15 if(listen_sock<0)
16 {
17 perror("socket");
18 return 0;
19 }
20 cout<<listen_sock<<endl;
21 return 0;
22 }
2.服务端绑定套接字bind接口
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号。
- bind()成功返回0,失败返回-1。
- bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
1 #include<unistd.h>
2 #include<iostream>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/socket.h>
6 #include<arpa/inet.h>
7 #include<error.h>
8 #include<stdio.h>
9 using namespace std;
10
11 int main()
12 {
13 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
14
15 if(listen_sock<0)
16 {
17 perror("socket");
18 return 0;
19 }
20 cout<<listen_sock<<endl;
21
22 struct sockaddr_in addr;
23
24 addr.sin_family=AF_INET;
25 addr.sin_port=htons(20000);
26 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
27 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");这个地址包含所有本地网卡地址
28 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
29
30 if(ret<0)
31 {
32 perror("bind");
33 return 0;
34 }
35
36 return 0;
37 }
3.服务端监听套接字listen接口
int listen(int sockfd, int backlog);
- sockfd:套接字描述符
- backlog:已完成连接队列的大小
- 返回值:
成功:0
失败:-1
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5),。
- listen()成功返回0,失败返回-1。
当客户端和服务端进行三次握手的时候会存在两种状态:连接还未建立和连接已建立,此时操作系统内核中就会存在两个队列:未完成连接队列和已完成连接队列。
如上图若客户端只完成①或①②则此连接在未完成连接队列中,当完成三次握手后会由未完成连接队列放到已完成连接队列,而backlog就是已完成连接队列的大小,backlog影响了服务端并发接收连接的能力。
eg:假设backlog=1,服务端不accepct接收连接,此时有三个客户端都完成了三次握手,则必有一个客户端连接进入已完成连接队列中,由于已完成连接队列空间不够,所以剩余两个客户端的连接只能放入未完成连接队列中。
1 #include<unistd.h>
2 #include<iostream>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/socket.h>
6 #include<arpa/inet.h>
7 #include<error.h>
8 #include<stdio.h>
9 using namespace std;
10
11 int main()
12 {
13 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
14
15 if(listen_sock<0)
16 {
17 perror("socket");
18 return 0;
19 }
20 cout<<listen_sock<<endl;
21
22 struct sockaddr_in addr;
23
24 addr.sin_family=AF_INET;
25 addr.sin_port=htons(20000);
26 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
27 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");这个地址包含所有本地网卡地址
28 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
29
30 if(ret<0)
31 {
32 perror("bind");
33 return 0;
34 }
35
36 ret=listen(listen_sock,1);
37 if(ret<0)
38 {
39 perror("listen");
40 return 0;
41 }
42
43 while(1)
44 {
45 sleep(1);
46 }
47
48 return 0;
49 }
4.服务端接收链接套接字accept接口
从已经完成连接队列中获取已经完成三次握手的连接,没有连接时,调用accept会阻塞。
int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);
- sockfd:套接字描述符(listen_sockfd)
- addr:客户端地址信息结构(客户端IP,客户端的端口)
- addrlen:客户端地址信息结构的长度
- 返回值:
成功:返回新连接的套接字描述符
失败:返回-1
三次握手的时候是对listen_sockfd进行操作,当调用accept()会在Tcp服务端内部创建一个新的套接字new_sockfd,三次握手之后的数据收发都是多new_sockfd进行操作,如下图所示:
- 三次握手完成后, 服务器调用accept()接受连接。
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。
5.客户端连接套接字connect接口
int connect(int sockfd, const struct sockaddr * addr,socklen_t addrlen);
- sockfd:套接字描述符(listen_sockfd)
- addr:服务端地址信息结构(服务端IP,服务端的端口)
- addrlen:服务端地址信息结构的长度
- 返回值:
成功:返回0
小于0,连接失败
- 客户端需要调用connect()连接服务器。
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址。
6.TCP发送接口send接口
ssize_t send(int sockfd, const void * buf, size_t len, int flags);
- sockfd:套接字描述符(new_sockfd)
- buf:待要发送的数据
- len:发送数据的长度
- flags:
0:阻塞发送
MSG_OOB:发送带外数据
返回值:
大于0:返回发送的字节数量
-1:发送失败
带外数据:即在紧急情况下所产生的数据,会越过前面进行排队的数据优先进行发送。
7.TCP接收接口recv接口
ssize_t recv(int sockfd, void * buf, size_t len, int flags);
- sockfd:套接字描述符(new_sockfd)
- buf:将接收的数据放到buf
- len:buf的最大接收能力
- flags:0:阻塞发送;如果客户端没有发送数据,调用recv会阻塞
- 返回值:
大于0:正常接收了多少字节数据
等于0:对端将连接关闭了
小于0:接受失败
8.TCP关闭接口close接口
3.TCP的连接建立
我们可以用cmd工具telnet模仿TCP三次握手建立连接,在cmd窗口输入 “tenlet + 公网IP + 端口号” 即可模拟测试:
按下回车键出现以下现象则连接成功:
当我们使用telnet与服务器建立三次连接(即进行三次三次握手)我们会看到,当我们查看服务器端口的使用情况是时会看到如下情况:
虽然我们在代码中将已完成连接队列的大小设为1,但上图已完成连接队列中却放了两个已完成连接,正常情况当我们就backlog设为1,已完成连接队列中只能放一个已完成连接,那么为什么会出现种情况呢?原因是操作系统内核中判断已完成队列是否已满的逻辑是如下所示:
所以我们设置的bakclog=1,向已完成连接队列中放入一个已完成连接,queue.size= 1不大于backlog=1,所以再向已完成连接队列中放入一个已完成连接,此时queue.size= 2大于backlog=1,不再放入,所以就出现如上图所示现象,虽然我们将backlog设为1,但已完成连接队列中却有两个已完成连接。
4.单进程的TCP的发送和接收数据
1.客户端代码
客户端流程:创建套接字,发起连接connect,发送和接收:
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 using namespace std;
11
12 int main()
13 {
14 int sockfd=socket(AF_INET,SOCK_STREAM,0);
15
16 if(sockfd<0)
17 {
18 perror("socket");
19 return 0;
20 }
21 cout<<sockfd<<endl;
22
23 struct sockaddr_in addr;
24
25 addr.sin_family=AF_INET;
26 addr.sin_port=htons(20000);
27 addr.sin_addr.s_addr=inet_addr("1.14.165.138");
28 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");这个地址包含所有本地网卡地址
29 int ret=connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
30
31 if(ret<0)
32 {
33 perror("connect");
34 return 0;
35 }
36
37 if(ret<0)
38 {
39 perror("connect");
40 return 0;
41 }
42
43
44 while(1)
45 {
46 char buf[1024]="i am client呀呀呀!";
47
48 send(sockfd,buf,strlen(buf),0);
49 memset(buf,'\0',sizeof(buf));
50 //接收
51 ssize_t recv_size=recv(sockfd,buf,sizeof(buf)-1,0);
52 if(recv_size<0)
53 {
54 perror("recv");
55 return 0;
56 }
57 else if(recv_size==0)
58 {
59 cout<<"close peer connect"<<endl;
60 close(sockfd);
61 continue;
62 }
63
64 cout<<"buf:"<<buf<<endl;
65
66
67
68
69 sleep(1);
70 }
71
72 return 0;
73 }
2.服务端端代码
服务端流程:创建侦听套接字,绑定地址信息,监听,接收新连接accept,接收,发送:
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 using namespace std;
11
12 int main()
13 {
14 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
15
16 if(listen_sock<0)
17 {
18 perror("socket");
19 return 0;
20 }
21 cout<<listen_sock<<endl;
22
23 struct sockaddr_in addr;
24
25 addr.sin_family=AF_INET;
26 addr.sin_port=htons(20000);
27 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
28 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");这个地址包含所有本地网卡地址
29 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
30
31 if(ret<0)
32 {
33 perror("bind");
34 return 0;
35 }
36
37 ret=listen(listen_sock,1);
38 if(ret<0)
39 {
40 perror("listen");
41 return 0;
42 }
43
44 struct sockaddr_in cli_addr;
45 socklen_t cli_addrlen=sizeof(cli_addr);
46 int newsockfd=accept(listen_sock,(struct sockaddr*)&cli_addr,&cli_addrlen);
47 if(newsockfd<0)
48 {
49 perror("accept");
50 return 0;
51 }
52
53 cout<<"i accept new connect form client:"<<inet_ntoa(cli_addr.sin_addr)<<" :"<<ntohs(cli_addr.sin_port)<<endl;
54 while(1)
55 {
56 char buf[1024]={0};
57
58 ssize_t recv_size=recv(newsockfd,buf,sizeof(buf)-1,0);
59 if(recv_size<0)
60 {
61 perror("recv");
62 return 0;
63 }
64 else if(recv_size==0)
65 {
66 cout<<"close peer connect"<<endl;
67 close(newsockfd);
68 continue;
69 }
70
71 cout<<"buf:"<<buf<<endl;
72
73 memset(buf,'\0',sizeof(buf));
74 strcpy(buf,"i am serve!!!");
75 send(newsockfd,buf,strlen(buf),0);
76
77
78
79 sleep(1);
80 }
81
82 return 0;
83 }
- 由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。
- 客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动多个客户端, 就会出现端口号被占用导致不能正确建立连接。
- 服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦。
5.单进程的TCP的发送和接收数据的问题
再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信。
分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求
通过pstack查看服务端阻塞在recv处,因为服务端accept接收新连接在while循环外面,所以服务端在进行一次连接之后会进入while循环内部,不能再接收新连接(虽然客户端2和服务端完成了三次握手建立了新连接,但服务端无法接收连接,此时客户端则无法收到服务端的数据)
TCP单进程存在的问题:当存在多个客户端与服务器进行通信时,可能会出现recv阻塞或accept阻塞。
若将accept放入while循环里呢?
将accpect放入while循环中,则每个客户端只能收到一条,当客户端与服务端建立连接,向服务端发送数据服务端,服务端接收数据并回复客户端,此时服务端将回到while循环的开始阻塞在accept处(因为之前已经接收客户端发起的连接,当第二次accept时,已完成连接队列中就是空队列)。
- 解决办法:
- 多线程。
- 多进程。
6.多线程的TCP的发送和接收数据
多进程的客户端代码和单进程是一样的,单进程服务端父进程负责accept,子进程负责数据的接收和发送,需要注意的是,父进程一定要进程等待,防止子进程先于父进程退出使子进程变为僵尸进程,而父进程不能直接父进程的逻辑处使用wait或waitpid进行等待,因为阻塞等待,若子进程一直不退出,则父进程一直在等待,永远无法接收新连接,我们 需要使用需要使用自定义信号处理方式将SIGCHLD信号重新定义,当子进程退出发出SIGCHLD信号时,父进程则对子进程的资源进行回收。
1.客户端代码
创建套接字,发起连接,发送和接收
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 using namespace std;
11
12 int main()
13 {
14 int sockfd=socket(AF_INET,SOCK_STREAM,0);
15
16 if(sockfd<0)
17 {
18 perror("socket");
19 return 0;
20 }
21 cout<<sockfd<<endl;
22
23 struct sockaddr_in addr;
24
25 addr.sin_family=AF_INET;
26 addr.sin_port=htons(20000);
27 addr.sin_addr.s_addr=inet_addr("1.14.165.138");
28 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");这个地址包含所有本地网卡地址
29 int ret=connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
30
31 if(ret<0)
32 {
33 perror("connect");
34 return 0;
35 }
36
37 if(ret<0)
38 {
39 perror("connect");
40 return 0;
41 }
42
43
44 while(1)
45 {
46 char buf[1024]="i am client!";
47
48 send(sockfd,buf,strlen(buf),0);
49 memset(buf,'\0',sizeof(buf));
50 //接收
51 ssize_t recv_size=recv(sockfd,buf,sizeof(buf)-1,0);
52 if(recv_size<0)
53 {
54 perror("recv");
55 return 0;
56 }
57 else if(recv_size==0)
58 {
59 cout<<"close peer connect"<<endl;
60 close(sockfd);
61 continue;
62 }
63
64 cout<<"buf:"<<buf<<endl;
65
66
67
68
69 sleep(1);
70 }
71
72 return 0;
73 }
2.服务端代码
服务端主要流程:创建侦听套接字,绑定地址信息,监听,接收新连接,创建子进程,接收,发送
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 #include<pthread.h>
11 using namespace std;
12
13 struct ThreadInfo
14 {
15 int _newSockFd;
16 };
17
18 void* TcpThreadStart(void* arg)
19 {
20 pthread_detach(pthread_self());
21 struct ThreadInfo* ti=(struct ThreadInfo*)arg;
22 int newsockfd=ti->_newSockFd;
23
24 while(1)
25 {
26 char buf[1024]={0};
27
28 ssize_t recv_size=recv(newsockfd,buf,sizeof(buf)-1,0);
29 if(recv_size<0)
30 {
31 perror("recv");
32 return 0;
33 }
34 else if(recv_size==0)
35 {
36 cout<<"close peer connect"<<endl;
37 close(newsockfd);
38 continue;
39 }
40
41 cout<<"buf:"<<buf<<endl;
42
43 memset(buf,'\0',sizeof(buf));
44 strcpy(buf,"i am serve!!!");
45 send(newsockfd,buf,strlen(buf),0);
46
47 }
48 delete ti;
49 return NULL;
50 }
51 int main()
52 {
53 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
54
55 if(listen_sock<0)
56 {
57 perror("socket");
58 return 0;
59 }
60 cout<<listen_sock<<endl;
61
62 struct sockaddr_in addr;
63
64 addr.sin_family=AF_INET;
65 addr.sin_port=htons(20000);
66 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
67 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");这个地址包含所有本地网卡地址
68 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
69
70 if(ret<0)
71 {
72 perror("bind");
73 return 0;
74 }
75
76 ret=listen(listen_sock,1);
77 if(ret<0)
78 {
79 perror("listen");
80 return 0;
81 }
82
83 while(1)
84 {
85 struct sockaddr_in cli_addr;
86 socklen_t cli_addrlen=sizeof(cli_addr);
87 int newsockfd=accept(listen_sock,(struct sockaddr*)&cli_addr,&cli_addrlen);
88 if(newsockfd<0)
89 {
90 perror("accept");
91 return 0;
92 }
93
94 cout<<"i accept new connect form client:"<<inet_ntoa(cli_addr.sin_addr)<<" :"<<ntohs(cli_addr.sin_port)<<endl;
95
96 struct ThreadInfo* ti =new ThreadInfo;
97 ti->_newSockFd=newsockfd;
98
99
100 pthread_t tid;
101 ret=pthread_create(&tid,NULL,TcpThreadStart,(void*)ti);
102 if(ret<0)
103 {
104 perror("pthread_create");
105 close(newsockfd);
106 delete ti;
107 continue;
108 }
109
110
111
112
113 sleep(1);
114 }
115
116 return 0;
117 }
7. 多进程的TCP的发送和接收数据
1.客户端代码
1 #include<unistd.h>
2 #include<string.h>
3 #include<iostream>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<error.h>
9 #include<stdio.h>
10 using namespace std;
11
12 int main()
13 {
14 int sockfd=socket(AF_INET,SOCK_STREAM,0);
15
16 if(sockfd<0)
17 {
18 perror("socket");
19 return 0;
20 }
21 cout<<sockfd<<endl;
22
23 struct sockaddr_in addr;
24
25 addr.sin_family=AF_INET;
26 addr.sin_port=htons(20000);
27 addr.sin_addr.s_addr=inet_addr("1.14.165.138");
28 // addr.sin_addr.s_addr=inet_addr("0.0.0.0");这个地址包含所有本地网卡地址
29 int ret=connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
30
31 if(ret<0)
32 {
33 perror("connect");
34 return 0;
35 }
36
37 if(ret<0)
38 {
39 perror("connect");
40 return 0;
41 }
42
43
44 while(1)
45 {
46 char buf[1024]="i am client!";
47
48 send(sockfd,buf,strlen(buf),0);
49 memset(buf,'\0',sizeof(buf));
50 //接收
51 ssize_t recv_size=recv(sockfd,buf,sizeof(buf)-1,0);
52 if(recv_size<0)
53 {
54 perror("recv");
55 return 0;
56 }
57 else if(recv_size==0)
58 {
59 cout<<"close peer connect"<<endl;
60 close(sockfd);
61 continue;
62 }
63
64 cout<<"buf:"<<buf<<endl;
65
66
67
68
69 sleep(1);
70 }
71
72 return 0;
73 }
2.服务端代码
1 #include<iostream>
2 #include<errno.h>
3 #include<stdlib.h>
4 #include<sys/socket.h>
5 #include<sys/types.h>
6 #include<unistd.h>
7 #include<string.h>
8 #include<arpa/inet.h>
9 #include<netinet/in.h>
10 #include<sys/wait.h>
11 #include<stdio.h>
12 using namespace std;
13
14 void signalcallback(int signo)
15 {
16 //当前的wait是进程等待的阻塞接口, 但是应用的场景一定是子进程退出之后,
17 //父进程收到了SIGCHLD信号之后, 才会回调sigcallback函数, 才会调用wait
18 cout<<"recv signo:"<<signo<<endl;
19 wait(NULL);
20 }
21 int main()
22 {
23 signal(SIGCHLD,signalcallback);
24 int listen_sock=socket(AF_INET,SOCK_STREAM,0);
25 if(listen_sock<0)
26 {
27 perror("socket");
28 return 0;
29 }
30
31 struct sockaddr_in addr;
32
33 addr.sin_family=AF_INET;
34 addr.sin_port=htons(20000);
35 addr.sin_addr.s_addr=inet_addr("172.16.0.9");
36
37 int ret=bind(listen_sock,(struct sockaddr*)&addr,sizeof(addr));
38 if(ret<0)
39 {
40 perror("bind");
41 return 0;
42 }
43
44
45 ret=listen(listen_sock,5);
46 if(ret<0)
47 {
48 perror("listen");
49 return 0;
50 }
51
52 while(1)
53 {
54 int new_sock=accept(listen_sock,NULL,NULL); //客服端的地址和端口号,这么传NULL,不关心客服端的端口号
55 if(new_sock<0)
56 {
57 continue;
58 }
59
60 //创建子进程
61 int pid=fork();
62 if(pid<0)
63 {
64 //创建子进程失败,但是接收新链接成功
65 close(new_sock);
66 continue;
67 }
68 else if(pid==0)
69 {
70 //child
71 close(listen_sock);
72
73 while(1)
74 {
75 //recv and send
76
77 char buf[1024]={0};
78
79 ssize_t recv_size=recv(new_sock,buf,sizeof(buf)-1,0);
80 if(recv_size<0)
81 {
82 perror("recv");
83 continue;
84 }
85 else if(recv_size==0)
86 {
87 cout<<"peer shutdown!"<<endl;
88 close(new_sock);
89
90 //子进程故障
91 //exit(1);
92
93 }
94
95 cout<<"client say:"<<buf<<endl;
96
97 memset(buf,'\0',sizeof(buf));
98 strcpy(buf,"i am serve");
99 send(new_sock,buf,strlen(buf),0);
100 }
101 }
102 else
103 {
104 close(new_sock);
105 }
106 }
107 return 0;
108 }
七、TCP协议通讯流程
- 服务器初始化:
- 调用socket, 创建文件描述符。
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败。
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备。
- 调用accecpt, 并阻塞, 等待客户端连接过来符。
- 建立连接的过程:
- 调用socket, 创建文件描述符。
- 调用connect, 向服务器发起连接请求。
- connect会发出SYN段并阻塞等待服务器应答; (第一次)。
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)。
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)。
这个建立连接的过程, 通常称为 三次握手。
- 数据传输的过程:
- 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据。
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待。
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答。
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求。
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去。
- 断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次)。
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次)。
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)。
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)。
这这个断开连接的过程, 通常称为 四次挥手。
- 在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段。
- 用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。
💬总结
以上就是今天要讲的内容,本文详细介绍了网络编程中UDP、和Tcp的编程等知识的使用,网络提供了大量的方法供我们使用,非常的便捷,我们务必掌握。希望大家多多支持!另外如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。加油啊!