目录
UDP通信程序的编写:
udp通信流程*:
接口认识:
字节序转换接口*:
查看网络连接状态的命令:
实现构建思路:
udp_srv.c:
udp_socket.hpp:
udp_client.cpp:
实现效果:
TCP通信程序的编写:
TCP通信操作流程:
接口:
实现思路:
tcp_socket.hpp:
tcp_client.cpp:
tcp_srv.cpp:
多线程版:
多进程版:
实现效果:
套接字编程: 即网络通信程序的编写.
网络中的通信都是两端主机之间的通信: 客户端和服务端
客户端:网络通信中用户的一端,是进行业务请求的一端,是主动发起请求的一端
服务端:网络通信中提供服务的一端,针对客户端请求进行处理的一端,是被动接收请求的一端
qq聊天并不是自己跟另一个手机或者电脑用户在通信,实际上是跟腾讯的服务器在进行通信,把一个消息发到某个群,其实是把数据发送给了服务器,描述了数据在哪个群,服务器就能找到群里有哪些用户,这些用户也登录了服务器,就可以把数据逐一发送给对应的主机。
不存在客户端与客户端的通信,不存在服务端与服务端的通信。
网络通信编程有: tcp协议通信程序的编写, udp通信程序的编写
tcp协议和udp协议的区别初识:
tcp协议∶传输控制协议--面向连接,可靠的字节流传输协议(确保数据安全有序到达对端)
tcp协议为了保证可靠传输,因此使用了很多处理机制来完成,因此传输性能相对于udp较低tcp协议的应用场景:安全要求大于性能要求。比如文件传输
udp协议:用户数据报协议--无连接,不可靠的数据报传输协议(不确保数据安全有序到达对端)
udp协议不需要保证可靠传输,只需要尽管传输就行因此传输性能要更高一些
udp协议的应用场景:性能要求大于安全要求。比如视频数据传输(要求不卡)
网络传输中的数据都会具有五元组: sip(源端ip地址), sport(源端端口),dip(对端ip地址),dport(对端端口),protocol(使用的协议)
客户端要给服务端发送数据,他怎么知道服务端是谁?
服务端都会提前将自己的地址信息封装在客户端程序中。也正是因为如此,服务端的地址信息通常都不能随意改变。
1. 创建套接字: int socket(int domain, int type, int protocol)
domain: 地址域类型,决定通信使用的地址结构, IPV4地址域类型为AF_INET
type:套接字类型,决定套接字传输方式, SOCK_DGRAM:无连接的,不可靠的数据报传输服务(UDP). SOCK_STREAM:基于连接的有序的可靠的字节流传输服务(TCP).
protocol: 决定使用的协议类型, IPPROTO_TCP, IPPROTO_UDP.
返回值: 套接字的操作句柄(文件描述符),用于后续接口传参操作. 失败返回-1.
2. 绑定地址信息: int bind(int sockfd, struct sockaddr *addr, socklen_t len)
sockfd: socket()返回的操作句柄, 决定了给哪个套接字绑定地址信息
addr: 绑定的地址信息. struct sockaddr是通用地址结构, 一般不使用这个类型根据上面socket()的domain对应的地址结构决定, 传参时强转为通用的就行. IPV4地址结构为sockaddr_in, IPV6为sockaddr_in6. (因为使用的地址域多种多样, 地址结构不同,但bind接口只有一个,所以统一用通用的作为传入,bind接口内部会根据前2个字节决定传入的地质结构该如何解析.)
len: 地址信息长度. 指定前一个参数的长度, 防止访问越界
所以常写为: bind(sockfd,(sruct sockaddr*)& addr, sizeof(addr))
返回值: 成功返回0,失败返回-1.
3.发送数据:
ssize_t sendto(int sockfd, void* buf, int len, itn flag, struct sockaddr* peer_addr, socklen_t len)
sockfd: socked()返回的操作句柄.
buf: 要发送的数据首地址.
len: 发送数据的长度.
flag: 标志位, 0默认为阻塞操作.
peer_addr: 对端地址信息(接收数据那端). 也是根据对应的强转为通用的
addrlen: 地址信息长度
返回值: 成功返回实际发送的数据长度, 失败返回-1.
4.接收数据:
ssize_t recvfrom(int sockfd, void* buf, int len, int flag, struct sockaddr* peer_addr, socklen_t* addrlen)
sockfd: socked()返回的操作句柄.
buf: 要存放数据的一块缓冲区空间首地址.
len: 要获取的数据的长度.
flag: 标志位, 0默认为阻塞操作. 缓冲区没有数据则等待.
peer_addr: 获取数据的源端地址信息(发送数据那端), 创建一个strcut sockaddr_in传入recvfrom接口内部会使之接收, 也是根据对应的强转为通用.(客户端实际上不需要获取服务端地址信息,因为本来就封装有了)
addrlen: 输入输出参数, 用于指定要获取的地址长度以及返回的实际长度
返回值: 成功返回实际收到的数据长度, 失败返回-1.
5.关闭套接字: int close(int sockfd)
网络通信使用的是网络字节序, 因此需要字节序转换.
16位数据主机与网络字节序转换: uint16_t htons(unit16_t port), uint16_t ntohs(uint16_t port)
32位数据主机与网络字节序转换: uint32_t htonl(uint32_t ip), uint32_t ntohl(uint32_t ip)
host to net 主机转网络, net to host网络转主机, 不限于port和ip的转换.
绑定时,把指定端口用htons()转后赋给sin_port. 不能用htonl,且用自己的主机地址(虚拟机地址)
接收数据时把数据发送端的地址信息中sin_port用ntohs()转出来.
将点分十进制的字符串ip地址转换为网络字节序整数的ip地址(IPV4):
in_addr_t inet_addr(char* ip) 转之后赋给sin_addr.s_addr
将网络字节序整数的ip地址转换为点分十进制的字符串ip地址(IPV4):
char* inet_ntoa(struct in_addr sin_addr), 注意这个参数不需要深入到in_addr里的in_addr_t
而上面的返回值是深入到in_addr_t
inet_ntop() / inet_pton(): IPV4和IPV6都可以
netstat
-a查看所有信息
-n不以服务名称显示(使22端口不被显示成为ssh,127.0.0.1不被显示为localhost等..)
-p查看对应网络状态信息所属的进程
-t tcp连接信息;
-u udp连接信息
首先使用C语言编写udp服务端程序---了解接口的实际使用与细节流程
接下来使用C++封装一个UdpSocket类, 客户端通过实例化这个类实现客户端的搭建. 因为客户端不需要知道接收发送等接口如何实现,只需要去使用.
上图展现了不同客户端, 由于没绑定, 不同的客户端发送消息给服务端操作系统就会自己分配不同的地址信息(端口,由于咱虚拟机ip不是动态分配的固定192.168.170.128所以ip地址没变).
开始监听:
int listen(int sockfd, int backlog)
sockfd: 要使之开始监听的套接字描述符
backlog: 服务端在同一时间内能处理的最大客户端连接请求数量.
(SYN泛洪攻击: 恶意伪造ip地址, 向服务器发送大量连接请求, 这样服务器就会不断的创建大量的通信套接字, 不如不做最大连接请求数量, 有可能瞬间资源耗尽导致系统崩溃.)
返回值: 成功返回0, 失败返回-1.
客户端发送连接请求:
int connect(int sockfd, struct sockaddr* addr, socklen_t len)
sockfd: 套接字描述符
addr: 指定发给哪个服务端该服务端的地址信息, ipv4通信使用struct sockaddr_in结构
len: 地址信息长度.
返回值: 成功返回0,失败返回-1.
服务端获取新建连接请求:
int accept(int sockfd, struct sockaddr* addr, socklen_t* len)
sockfd: 监听套接字描述符(监听套接字就是只用来处理连接请求复制出新套接字的套接字)
addr: 客户端的地址信息, 将该客户端的地址信息放入这个addr中.
len: 输入输出参数, 指定要获取的地址长度, 以及返回实际获取的信息长度
返回值: 成功返回新建连接的通信套接字描述符, 失败返回-1.
发送数据(和udp的sendto不同):
ssize_t send(int sockfd, void* data,int len,int flag)
sockfd: 套接字描述符
data:要发送的数据的首地址
flag: 表示位, 通常置0, 表示阻塞发送(无法封装信息进入发送缓冲区则等待)
返回值: 成功返回实际发送的数据长度, 失败返回-1.
接收数据(和udp的recvfrom不同):
ssize_t recv(int sockfd,void* buf, int len,int flag)
sockfd: 套接字描述符
buf: 缓冲区首地址,用于存放收到的数据
len: 想要获取的数据长度
flag: 标志位, 通常置0,阻塞接收(接收缓冲区没有数据则等待)
返回值: 成功返回实际获取的数据长度, 失败返回-1, 连接断开返回0
关闭套接字:
int close(int sockfd);
其他的字节序转换接口也与udp一样.
与udp实现思路不同的是, tcp客户端和服务端都通过实例化一个tcpsocket类来实现.
且udp是无连接的, 服务端只需要把接收数据放入循环中, 客户端只要发送的地址信息是该服务端就可以实现与多客户端的不断收发数据(上面我们实现的逻辑是while(收,发), 只要收到信息, 可以是不同客户端的, 服务端收到后再发送回复就可以循环回到收数据). 而tcp服务端不同, 服务端有获取新建连接请求这步, 如果while(获取新建请求->收->发)这样操作就会导致回复第一个客户端后, 没有新的客户端发送来新建连接请求则阻塞无法再收发数据,这样就只能每个客户端只能通信一次, 若是: 获取新建连接请求->while(收->发), 这样则只实现了与单个客户端多次通信,但又无法实现与多个客户端通信了.
为了解决以上问题: 我们可以采用多进程或者多线程的多执行流方案, 让主执行流只负责获取新建连接请求, 新建连接成功后创建一个执行流, 将新的的套接字传入, 让这个执行流只负责与某个固定的客户端通信.
多线程方案注意事项(详情看注释):
线程之间共享进程的文件描述表, 将套接字传入线程入口函数后是否会超出作用域,生命周期结束被释放. 因此得从堆上申请套接字(new出来), 之后使用完再手动释放.
多进程方案注意事项(详情看注释):
子进程复制父进程信息, 代码共享,但数据独有, 像之前的文件描述符一样父子进程有各自的读端和写端(例如管道通信那), 需各自关闭sockfd.但也因为数据独有通信套接字不会被父进程释放不需要用new. 且父进程得先修改SIG_CHLD信号, 避免子进程退出后产生僵尸进程.
差别只有上面注意事项说的那几个.