1. 网络进程间通信
本地进程可通过进程号(Process ID, PID)唯一标识,网络中如何进行进程间通信呢?
首先要解决的问题是如何唯一标识一个进程,其实网络中TCP/IP已经解决了这个问题。
网络层 IP地址可以唯一标识网络中的主机
传输层 “协议+端口”可以唯一标识主机中的应用程序(进程)
“IP地址+协议+端口”可以标识网络的进程,网络中进程通信即可实现。
网络五组元信息:一条网络数据一定包含5部分信息
(1)源 IP 地址:表示该条信息来自于哪个机器
(2)源端口:表示该条信息来自于哪个进程
(3)目的 IP 地址:表示该条信息去往哪一个机器
(4)目的端口:表示该条信息去往哪一个进程
(5)协议:双方网络数据采用的具体网络协议
2. UDP的简单特性
UDP的简单特性:无连接,不可靠,面向数据报
(1)无连接:UDP客户端给服务端发送消息的时候,不需要和服务端先建立连接,直接发送(客户端也不清楚服务端是否真正在线)
(2)不可靠:UDP并不会保证数据是可靠有序到达对端
(3)面向数据报:UDP数据不管是和应用层还是网络层,都是整条数据交付
3. TCP的简单特性
TCP的简单特性:面向连接、可靠传输、面向字节流
(1)面向连接:双方在发送网络数据之前必须先建立连接,再进行发送
(2)可靠传输:保证数据是可靠并且有序的到达对端
(3)面向字节流:多次发送的数据在网络传输过程当中没有明显的数据边界。比如先发123,再发456,另一端收到的就是123456,它没有间隔。
4. 字节序
4.1 概念
小端字节序:低位存储在低地址
大端字节序:低位存储在高地址
网络字节序:采用的是大端字节序
主机字节序:根据机器本身的字节序
机器是大端:主机字节序为大端
机器是小端:主机字节序为小端
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
4.2 主机字节序与网络字节序的转换
头文件:#include<arpa/inet.h>
(1)主机字节序转换为网络字节序
端口:
unit16_t htons(unit16_t hostshort);
IP:
in_addr_t inet_addr(const char *cp);
将点分十进制的IP字符串转换成为unit32_t的整数,再将unit32_t的整数从主机字节序转换成为网络字节序
addr.sin_addr.s_addr 中的 s_addr 的返回值是一个无符号32位整数
(2)网络字节序转换为主机字节序
端口:
unit16_t ntohs(unit16_t netshort);
IP:
char *inet_ntoa(struct in_addr in); //“ntoa"的含义是"network to ascii”
将IP地址从网络字节序转换成为主机字节序,将unit32_t的整数转换成为点分十进制的字符串
inet_ntoa不是线程安全的函数
inet_ntoa()将结构体in_addr作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的指针。它是一个由inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的IP地址。例如:
char *a1, *a2;
……
a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */
a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */
printf("address 1: %s\n",a1);
printf("address 2: %s\n",a2);
输出如下:
address 1: 132.241.5.10
address 2: 132.241.5.10
5. UDPsocket
5.1 什么是socket?
socket被称为套接字,用来实现网络进程之间的通信
socket 是在应用层和传输层之间的一个抽象层,socket把TCP/IP层复杂的操作抽象为简单的接口供应用层调用,以实现进程在网络中的通信
socket用于描述IP地址和端口,是一个通信链的句柄,用来实现不同虚拟机或物理机之间的通信。应用程序通过socket向网络发出请求或应答请求。网络中两个进程通过一个双向的通信连接实现数据的交换,建立网络通信连接至少需要一对socket,连接的一端称为一个socket。
5.2 socket常见的API函数
5.2.1 创建套接字
头文件:#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址域
指定网络层使用什么样的协议
AF_INET:使用ipv4版本的ip协议
AF_INET6:使用ipv6版本的ip协议
AF_UNIX:本地域套接字(适用于一台机器的两个进程,进行进程间通信)
type:创建套接字的类型
UDP:SOCK_DGRAM:用户数据报套接字
TCP:SOCK_STREAM:流式套接字
protocol:表示使用的协议
0:采用套接字类型对应的默认协议
SOCK_DGRAM:默认的协议是UDP
SOCK_STREAM:默认的协议是TCP
宏定义:IPPROTO_UDP:UDP协议
宏定义:IPPROTO_TCP:TCP协议
返回值:返回套接字描述符,本质上就是文件描述符
5.2.2 绑定地址信息
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符,socket函数的返回值
addr:地址信息结构
struct sockaddr:通用的结构体类型,具体采用的是各个协议自己的数据结构
返回值:返回套接字描述符,本质上就是文件描述符
在使用bind函数之前,需要创建协议所需要的地址信息结构体
struct sockaddr_in addr; //ipv4所用的结构体是struct sockaddr_in
addr.sin_family = AF_INET; //地址域信息
addr.sin_port = htons(20001); //端口
addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //ip地址
5.2.3 UDP发送数据
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:当前消息的目标地址信息结构(消息要发送到哪里去)
addr_len:地址信息结构长度
返回值:成功:返回发送的字节数
失败:-1
5.2.4 UDP接受数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:套接字描述符
buf:将接受的数据存放到buf中
len:最大接受能力
flags:0:表示阻塞接受
src_addr:表示数据从哪一个地址信息结构发来的(消息从哪一个ip和端口来的)
addr_len:地址信息结构长度
返回值:成功:返回接收的字节数
失败:-1
5.3 UDPsocket编程流程
客户端不需要绑定地址信息(ip地址+端口),让操作系统(即sendto函数)自己去绑定,因为自己绑定客户地址信息会导致当前客户端程只能在一台机器中运行一个进程,不能多开
5.4 UDPsocket编程
5.4.1 服务端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
//socket编程需要包含的头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//创建套接字
if(sockfd < 0)
{
//判断
perror("socket");
return 0;
}
printf("socket : %d\n", sockfd);//socket的返回值是int类型,用%d输出
//ipv4使用的结构体
struct sockaddr_in addr;
addr.sin_family = AF_INET;//地址域信息,这里采用ipv4的ip协议
addr.sin_port = htons(28989);//端口
addr.sin_addr.s_addr = inet_addr("172.21.0.9");//私网ip地址,私网ip地址才是描述这台机器的,发送消息需要公网ip
//sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要是网络字节顺序
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));//绑定地址信息
if(ret < 0)
{
perror("bind");
return 0;
}
while(1)
{
char buf[1024] = {0};
struct sockaddr_in peer_addr;
socklen_t len = sizeof(peer_addr);
ssize_t recv_size = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer_addr, &len);
if(recv_size < 0)
{
continue;
}
printf("recv msg \"%s\" from %s:%d\n", buf, inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
memset(buf, '\0', sizeof(buf));//内存初始化
sprintf(buf, "welcome client %s:%d", inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&peer_addr, sizeof(peer_addr));
}
close(sockfd);
return 0;
}
5.4.2 客户端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
//socket编程需要包含的头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sockfd < 0)
{
perror("socket");
return 0;
}
printf("socket : %d\n", sockfd);
while(1)
{
char buf[1024] = "i am client";
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(28989);
dest_addr.sin_addr.s_addr = inet_addr("82.157.94.99");//发送消息需要公网ip
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
memset(buf, '\0', sizeof(buf));
struct sockaddr_in peer_addr;
socklen_t len = sizeof(peer_addr);
ssize_t recv_size = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer_addr, &len);
if(recv_size < 0)
{
continue;
}
printf("recv msg %s from %s:%d\n", buf, inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
sleep(1);
}
close(sockfd);
return 0;
}
UDP无需建立连接,只能客户端给服务端发送消息(因为服务端不知道客户端的ip地址信息的)
需要先启动服务端,客户端才能发消息
6. TCPsocket
6.1 TCPsocket编程流程
6.2 TCP发送接收缓冲区
6.3 编程接口
6.3.1 socket:创建套接字
头文件:#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:地址域,指定网络层使用什么样的协议
AF_INET:使用ipv4版本的ip协议
AF_INET6:使用ipv6版本的ip协议
AF_UNIX:本地域套接字(适用于一台机器的两个进程,进行进程间通信)
type:创建套接字的类型
UDP:SOCK_DGRAM:用户数据报套接字
TCP:SOCK_STREAM:流式套接字
protocol:表示使用的协议
0:采用套接字类型对应的默认协议
SOCK_DGRAM:默认的协议是UDP
SOCK_STREAM:默认的协议是TCP
宏定义:IPPROTO_UDP:UDP协议
宏定义:IPPROTO_TCP:TCP协议
返回值:返回套接字描述符,本质上就是文件描述符
6.3.2 bind:绑定地址信息
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符,socket函数的返回值
addr:地址信息结构
struct sockaddr:通用的结构体类型,具体采用的是各个协议自己的数据结构
返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中
在使用bind函数之前,需要创建协议所需要的地址信息结构体
struct sockaddr_in addr; //ipv4所用的结构体是struct sockaddr_in
addr.sin_family = AF_INET; //地址域信息
addr.sin_port = htons(20001); //端口
addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //ip地址
6.3.3 listen:监听接口
int listen(int sockfd, int backlog);
sockfd:侦听套接字,socket函数的返回值
backlog:指定内核当中已完成连接队列的大小
返回值:成功则返回0 ,失败返回-1
结论:已完成连接队列的大小决定了服务端的并发连接数(指的是在同一时刻服务端能够处理的连接数量上限,并不是服务端能够接收连接的上限)
引申的问题:TCP服务端最大接收多少连接?
取决于操作系统对进程当中打开文件描述符的限制
可以用命令:ulimit -a进行查看和修改
core file size core文件的最大值为 0 blocks,
data seg size 进程的数据段可以任意大
file size 文件可以任意大
pending signals 最多有 7269 个待处理的信号
max locked memory 一个任务锁住的物理内存的最大值为64kB
max memory size 一个任务的常驻物理内存的最大值
open files 一个任务最多可以同时打开100001个文件
pipe size 管道的最大空间为4096字节(512×8)
POSIX message queues POSIX的消息队列的最大值为819200字节
stack size 进程的栈的最大值为8192字节
cpu time 进程使用的CPU时间
max user processes 当前用户同时打开的进程(包括线程)的最大个数为7269
virtual memory 没有限制进程的最大地址空间
file locks 所能锁住的文件的最大个数没有限制
6.3.4 connect:客户端的连接接口
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符
adr:要连接的服务端的地址信息结构
服务端的ip地址
服务端的端口
addrlen:地址信息结构长度
返回值:0:成功
-1:失败
6.3.5 accept:服务端接收连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:侦听套接字(socket函数的返回值)
addr:客户端的地址信息结构(出参:由accept函数返回客户端的地址信息结构)
addrlen:客户端的地址信息结构长度(出参:由accept函数返回客户端的地址信息结构长度)
返回值:返回新连接的套接字描述符
>= 0:成功
< 0:失败
6.3.6 send:TCP发送数据接口
ssize_t send (int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字描述符
客户端:socket函数的返回值
服务端:accept函数的返回值(切记不是侦听套接字)
buf:待要发送的数据
len:数据长度
flages:标志位
0:阻塞发送
MAG_PEEK:发送紧急数据(带外数据)
返回值:-1:发送失败
> 0:实际发送的字节数量
6.3.7 recv:TCP接收数据
ssize_t recv (int sockfd, void *buf, size_t len, int flags);
sockfd:套接字描述符
客户端:socket函数的返回值
服务端:accept函数的返回值(切记不是侦听套接字)
buf:将从TCP接收缓冲区当中接收的数据保存在buf当中
len:buf最大的接受能力
flags:0:阻塞接受
返回值:< 0:函数调用出错了
== 0:对端关闭连接
> 0:接收到的字节数量
6.3.8 closed:关闭连接
closed(int sockfd);
6.4 单线程TCPsocket编程
6.4.1 服务端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listen_sock < 0)
{
perror("socket");
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(28989);
//0.0.0.0 : 本地所有的网卡地址
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return 0;
}
ret = listen(listen_sock, 1);
if(ret < 0)
{
perror("listen");
return 0;
}
struct sockaddr_in cli_addr;
socklen_t cli_addrlen = sizeof(cli_addr);
int newsockfd = accept(listen_sock, (struct sockaddr*)&cli_addr, &cli_addrlen);
if(newsockfd < 0)
{
perror("accept");
return 0;
}
printf("accept new connect from client %s:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
while(1)
{
//接收
char buf[1024] = {0};
ssize_t recv_size = recv(newsockfd, buf, sizeof(buf) - 1, 0);
if(recv_size < 0)
{
perror("recv");
continue;
}
else if(recv_size == 0)
{
printf("peer close connect\n");
close(newsockfd);
continue;
}
printf("%s\n", buf);
memset(buf, '\0', sizeof(buf));
strcpy(buf, "i am server!!!");
send(newsockfd, buf, strlen(buf), 0);
}
close(listen_sock);
return 0;
}
6.4.2 客户端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(28989);
// 0.0.0.0 : 本地所有的网卡地址
addr.sin_addr.s_addr = inet_addr("82.157.94.99");//必须使用公网ip
int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("connect");
return 0;
}
while(1)
{
char buf[1024] = "i am client1111222";
send(sockfd, buf, strlen(buf), 0);
memset(buf, '\0', sizeof(buf));
//接收
ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
if(recv_size < 0)
{
perror("recv");
continue;
}
else if(recv_size == 0)
{
printf("peer close connect\n");
close(sockfd);
continue;
}
printf("%s\n", buf);
sleep(1);
}
close(sockfd);
return 0;
}
若出现连接被拒绝的错误,客户端需要更改自己的公网ip
6.4.3 单线程代码存在的问题
第一种情况:将accept放在while循环的外面,则整个tcp的服务端只能接受一个客户端的新连接(并不代表只能有一个客户端与服务端建立tcp连接)
第二种情况:将accept放在while循环的里面,则整个tcp的服务端可以接受多个客户端的新连接,但是每一个连接只能收发一次(每次循环的时候,都是去接受新的客户端连接了)
7. 公网ip与私网ip
(1)在socket编程当中,绑定的是本地网的ip地址(不需要关心的是公网ip还是私网ip)
(2)在互联网连接云服务器的时候,需要使用公网ip(云服务器厂商会帮助进行转换的)