UDP协议:向应用程序提供一种面向无连接的服务
TCP协议:提供一种面向连接的,可靠的数据传输服务
网络通信程序编写时,到底传输层使用UDP好还是TCP?
TCP ---- 传输控制协议 ——面向连接,可靠传输,面向字节流
UDP ----用户数据报协议——面向无连接,不可靠传输,面向数据包
TCP保证可靠传输,但是传输速度没有UDP快。
TCP应用于安全性要求高 / UDP应用于实时性高的场景
socket套接字编程:网络程序的编写
udp通信编程:
通信的编程流程:
客户端:1.创建套接字 2.绑定地址信息(不推荐主动) 3.发送数据 4.接收数据 5.关闭套接字
服务端:1.创建套接字 2.绑定地址信息 3.接收数据 4.发送数据 5.关闭套接字
客户端不主动绑定端口地址,是为了降低端口冲突的概率,但是服务端为什么必须要绑定地址?
如果我们发送请求必须要知道服务端地址和端口信息 ——(如果不知道服务端地址和信息就无法发送请求)客户端是主动连接,而服务器是等待连接
scoket接口介绍
1.创建套接字
int socket(int domain, int type, int protocol)
domain: 地址域——确定本次socket通信使用的版本 — AF_INET(ipv4网络协议) AF_INET6(ipv6网络协议)
type: 套接字类型——流式套接字(SOCK_STREAM) / 数据报套接字(SOCK_DGRAM)
protocol: 协议类型 (默认为0,流式默认TCP/数据报默认UDP)
返回值: 文件描述符 ——非负整数,套接字的操作句柄, 失败返回1
2.为套接字绑定地址信息
int bind(int sockfd, struct socketaddr* addr, socklen_t len);
sockfd: 创建套接字返回的操作句柄
addr: 要绑定的地址信息
len: 要绑定的地址信息长度
struct sockaddr{
sa_family_t sa_family;
char sa_data[4];
}
struct sockaddr_in{ //IPV4
sa_family_t sa_family; //地址域
in_port sin_port; //端口号
stuct in_addr{ in addr_t addr} sin_addr; //IP地址信息
}
bind可以绑定不同的地址结构,为了实现接口统一,因为用户定义地址结构的时候,定义自己需要的地址结构(例如 IPV4就使用struct sockaddr_in, IPV6就是用 struct sockaddr_in6) 但是进行绑定的时候,统一类型强转为sockaddr* 类型
3.接收数据
ssize_t recvfrom(int sockfd, char* buf, int len, int flag, stuct sockaddr* peer_addr, socklen_t* addrlen)
sockfd: socket的操作句柄
buf: 一块缓冲区,用于接收从接收缓冲区中取出数据;
len: 想要接收的长度
flag: 操作选项标识,默认为0,表示阻塞操作
pper_addr: 发送方地址长度
addlen: 想要获取的地址信息长度以及返回的实际长度
返回值: 成功返回实际接收到的数字字节长度, 失败返回-1
4.发送数据
ssize_t sendto(int sockfd, char* data, int len, int flag, struct sockaddr* peeraddr, socklen_t addrlen);
sockfd: socket的操作句柄
data: 要发送数据的首地址
len: 要发送数据长度
flag: 默认为0,表示阻塞操作
peeraddr: 接收方的地址信息
addrlen: 地址信息长度
5.关闭套接字
int close(int fd);
网络字节序转换接口
uint16_t htons(uint16_t htonshort); //主机字节序到网络字节序的转换
uint32_t htons(uint32_t htonlong);
uint16_t ntohs(uint16_t ntohshort); //网络字节序到主机字节序的转换
uint32_t ntohl(uint32_t ntohlong);
in_addr_t inet_addr(const char* cp); //将字符串的点分十进制转换成网络字节序的整数IP地址
char* inet_ntoa(struct in_addr in); //将网络字节序的整数IP地址,转换为点分十进制IP地址
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size); //将网络字节序的整数IP地址转换为字符串IP地址
int inet_pton(int af, const char* src, void* dst); //将字符串IP地址转换为网络字节序的IP地址
使用c++封装一个UdpSocket类,实例化每个对象都是一个udp通信套接字,并且通过成员接口实现udp通信流程
// udpsocket.hpp
#include<cstdio>
#include<string>
#include<unistd.h>
#include<netinet/in.h> //包含地址结构信息
#include<sys/socket.h> //套接字接口信息
#include<arpa/inet.h> //字节序转换接口
class UdpSocket{
public:
UdpSocket(int sockfd = -1):_sockfd(sockfd){}
bool Socket(){//创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(_sockfd < 0){
perror("socket error");
return false;
}
return true;
}
//为套接字绑定地址信息
bool Bind(const std::string& ip, uint16_t port){
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port); //htons将主机字节序短整型数据转换为网络字节序数据
addr.sin_addr.s_addr = inet_addr(ip.c_str()); //将字符串IP地址转换为网络字节序
socklen_t len = sizeof(struct sockaddr_in);
//bind(操作句柄, 地址信息, 地址信息长度)
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0){
perror("bind error");
return false;
}
return true;
}
//接收数据,获取发送端地址信息
bool Recv(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL){
//recv(套接字句柄, 接收缓冲区, 数据长度, 标志, 源端地址, 地址长度)
struct sockaddr_in peer_addr;
socklen_t len = sizeof(struct sockaddr_in);
char tmp[4096] = {0};
int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
if(ret < 0){
perror("recvfrom error!");
return false;
}
buf->assign(tmp, ret); //assign 从指定字符串中截取指定长度的数据到buf
if(port != NULL){
*port = ntohs(peer_addr.sin_port); // 从网络字节序到主机字节序的转换
}
if(ip != NULL){
*ip = inet_ntoa(peer_addr.sin_addr);//网络字节序到字符串IP地址转换
}
return true;
}
//发送数据
bool Send(const std::string& data, const std::string& ip, const uint16_t port){
//sendto(套接字句柄, 数据首地址, 数据长度, 标志, 对端地址信息, 地址信息长度)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
if(ret < 0){
perror("send error!");
return false;
}
return true;
}
//关闭套接字
bool Close(){
if(_sockfd > 0){
close(_sockfd);
_sockfd = -1;
}
return true;
}
private:
int _sockfd;
};
//udp_srv.cpp
#include<iostream>
#include<string>
#include"udpsocket.hpp"
#define CHECK_RET(q) if((q) == false){ return -1;}
int main(int argc, char* argv[]){
//argc表示程序运行参数的个数
//./udp_src 192.168.x.x 8080
if(argc != 3){
std::cout << "Usage: ./udp_srv ip port\n" << std::endl;
return -1;
}
uint16_t port = std::stoi(argv[2]);
std::string ip = argv[1];
UdpSocket srv_sock;
//创建套接字
CHECK_RET(srv_sock.Socket());
CHECK_RET(srv_sock.Bind(ip, port));
while(1){
std::string buf;
std::string peer_ip;
uint16_t peer_port;
CHECK_RET(srv_sock.Recv(&buf, &peer_ip, &peer_port));
std::cout << "client[ "<< peer_ip << ":" << peer_port << "]" <<"say: " << buf << std::endl;
buf.clear();
std::cout << "server say :";
std::cin >> buf;
CHECK_RET(srv_sock.Send(buf, peer_ip, peer_port));
}
CHECK_RET(srv_sock.Close());
}
//udp_cli.cpp
#include<iostream>
#include<string>
#include"udpsocket.hpp"
#define CHECK_RET(q) if((q) == false){ return -1; }
int main(int argc, char* argv[]){
//argc表示程序运行参数的个数
// // ./udp_src 192.168.x.x 8080
if(argc != 3){
std::cout << "Usage: ./udp_srv ip port" << std::endl;
return -1;
}
uint16_t srv_port = std::stoi(argv[2]);
std::string srv_ip = argv[1];
UdpSocket cli_sock;
//创建套接字
CHECK_RET(cli_sock.Socket());
while(1){
std::cout << "client say:";
std::string buf;
std::cin >> buf;
CHECK_RET(cli_sock.Send(buf, srv_ip, srv_port));
buf.clear();
CHECK_RET(cli_sock.Recv(&buf));
std::cout << "server say:" << buf << std::endl;
}
CHECK_RET(cli_sock.Close());
}
tcp通信编程:
1.创建套接字
int socket(int domain, int type, int protocol);
2.绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t len);
3.开始监听
int listen(int sockfd, int backlog);
sockfd:将套接字设置为监听状态。并且监听状态后可以开始接收客户端连接请求
backlog:同一时间的并发连接数,决定同一时间最多可以接收多少个客户端请求
4.获取新连接(从已完成连接队列中取出socket,并且返回这个socket的操作符句柄)
int accept(int sockfd, struct sockaddr* cli_addr, socklen_t len);
sockfd:监听套接字,表示要获取哪个tcp服务端套接字建立连接
cli_addr:这个新的套接字对应的客户端地址信息
返回值: 新建套接字的描述符----外部程序中的操作句柄
5.接收/发送数据(因为tcp通信套接字中已经标识了五元组,因此不需要接收数据的时候获取对方地址信息,发送数据的时候也不需要指定对方的地址信息)
ssize_t recv(int socket, char* buf, int len, int flag);
ssize_t send(int socket, char*buf, int len, int flag);
6.关闭套接字
int close(int sockfd);
7.向服务端发起连接请求
int connect(int sockfd, struct sockaddr* srv_addr, int len);
srv_addr: 服务端地址信息---给谁发送连接请求
封装一个TcpSocket类,每一个实例化对象都是一个socket通信连接,通过
//tcpsocket.hpp
#include<cstdio>
#include<string>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define MAX_LINSTEN 5
class TcpSocket{
public:
TcpSocket():_sockfd(-1){}
bool Socket(){
//socket(地址域, 套接字类型, 协议信息)
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0){
perror("socket error");
return false;
}
return true;
}
bool Bind(const std::string& ip, uint16_t port){
struct sockaddr_in addr;
addr.family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addrs.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, struct sockaddr*(sockaddr_in), len);
if(ret < 0){
perror("bind error");
return false;
}
return true;
}
bool Listen(int backlog = MAX_LISTEN){
//listen(套接字描述符, 最大并发数);
int ret = listen(_sockfd, backlog);
if(ret < 0){
perror("listen error");
return false;
}
return true;
}
bool Accept(TcpSocket* new_sock, std::string& ip = NULL, uint16_t* port = NULL){
//新建套接字描述符 = accpet(监听套接字描述符, 客户端地址信息, 地址信息长度)
struct sockaddr_in addr;
socklen_t len = sizeof(struct sockaddr_in);
int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
if(new_fd < 0){
perror("accpet error");
return false;
}
new_sock->sockfd = new_fd;
if(ip != NULL){
(*ip) = ntohs(addr.sin_addr);
}
if(port != NULL){
*port = ntohs(addr.sin_port);
}
return true;
}
bool Recv(std::string* buf){
//recv(描述符, 缓冲区首地址, 接收数据长度, 标志位0--阻塞)
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if(ret < 0){
perror("recv error");
return false;
}else if(ret == 0){//recv默认阻塞,没有数就会等待,返回0,标识等待断开
printf("connection broken\n");
return false;
}
buf->assgin(tmp, ret);
return true;
}
bool Send(const::string& data){
//send(描述符, 要发送的数据首地址, 数据长度, 标志位)
int ret = send(_sockfd, data.c_str(), data.size(), 0);
if(ret < 0){
perror("send error");
return false;
}
return true;
}
bool Close(){
if(_sockfd > 0){
close(_sockfd);
_sockfd = -1;
}
return true;
}
bool Connect(const std::string& ip, uint16_t port){
//向服务端发起连接
//connect(描述符, 服务端地址信息, 地址信息长度)
struct sockaddr_in addr;
addr.family = AF_INET;
addr.sin_port = port;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)addr, len);
if(ret < 0){
perror("connect error");
return false;
}
return true;
}
private:
int _sockfd;
};
while (1) {
TcpSocket new_sock;
bool ret = lst_sock.Accept(&new_sock);//通过监听套接字获取新建连接
if (ret == false) {
continue;//服务端不能因为一个新建套接字失败就退出
}
std::string buf;
new_sock.Recv(&buf);//通过新建连接与指定客户端进行通信
std::cout << "client say:" << buf << std::endl;
buf.clear();
std::cout << "sever say:";
std::cin >> buf;
new_sock.Send(buf);
}
lst_sock.Close();
这样的话只能与每个客户端获取一次,但如果把获取新连接放在while循环外面的话,又只能与一个客户端进行通信。
当前在一个执行流中完成了多个操作:获取新连接,接收数据,发送数据。然而这些操作都有可能导致流程阻塞,因为我们在固定流程下,有可能对没有数据到来的socket进行操作。因此导致阻塞
将执行流分为2类:
1.获取新连接
一旦获取到一个新连接,就启动一个新的执行流,让这个新的执行流去与客户端进行通信
①.因为没有新连接到来的阻塞,不会影响与客户端的通信
②.与客户端通信的阻塞,不会影响获取新连接
2.与客户端进行通信
//tcp_srv.cpp
#include<iostream>
#include<signal.h>
#include<sys/wait.h>
#include"tcpsocket.hpp"
void sigcb(int no) {
//SIGCHILD是一个非可靠信号有可能丢失,因此在一次信号处理中就需要处理到没有子进程退出为止
while(waitpid(-1, NULL, WNOHANG) > 0); //返回值大于0表示有子进程退出
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cout << "usage: ./tcp_srv ip port\n";
return -1;
}
signal(SIGCHLD, sigcb);
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());//创建套接字
CHECK_RET(lst_sock.Bind(ip, port));//开始连接
CHECK_RET(lst_sock.Listen());//开始监听
while (1) {
TcpSocket new_sock;
bool ret = lst_sock.Accept(&new_sock);//通过监听套接字获取新建连接
if (ret == false) {
continue;//服务端不能因为一个新建套接字失败就退出
}
int pid = fork();
if (pid == 0) {
while (1) {
std::string buf;
new_sock.Recv(&buf);//通过新建连接与指定客户端进行通信
std::cout << "client say:" << buf << std::endl;
buf.clear();
std::cout << "sever say:";
std::cin >> buf;
new_sock.Send(buf);
}
new_sock.Close();
exit(0);
}
new_sock.Close();
}
lst_sock.Close();
}
//tcp_cli.cpp
#include<iostream>
#include"tcpsocket.hpp"
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cout << "usage: ./tcp_cli ip port\n";
return -1;
}
std::string srv_ip = argv[1];
uint16_t srv_port = std::stoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket());//创建套接字
CHECK_RET(sock.Connect(srv_ip, srv_port));
while (1) {
std::string buf;
std::cout << "client say:" << buf;
std::cin >> buf;
sock.Send(buf);
buf.clear();
sock.Recv(&buf);
std::cout << "server say:" << buf << std::endl;
}
sock.Close();
return 0;
}