套接字编程涉及到两种网络传输协议 -- TCP / UDP
所以了解套接字编程之前,得先了解这两种传输协议的区别
TCP(传输控制协议) – 面向连接,可靠传输,面向字节流
UDP(用户数据报协议)–无连接,不可靠,面向数据报
TCP保证可靠安全传输,但是传输速度没有UDP块,所以TCP应用于安全性要求高的场景 / UDP应用于实时性要求高的场景
UDP通信流程
那为什么说客户端不推荐主动绑定地址呢?
因为客户端发送数据用什么源端地址无所谓,主动绑定,若被绑定的端口已经被占用,那么绑定就会失败
socket接口介绍
- 创建套接字
int socket(int domain,int type,int protocol)
domain: 地址域–确定本次socket通信使用哪种协议版本的地址结构 — 不同的协议版本有不同的额地址结构(AF_INET IPV4网络协议)
type: 套接字类型(流失套接字 – SOCK_STREAM / 数据报套接字 – SOCK_DGRAM)
protocol: 协议类型(通常就是传输协议的选择 IPPROTO_TCP / IPPROTO_UDP)
返回值: 文件描述符 – 非负整数 – 套接字所有其他接口的操作句柄,失败返回-1 - 为套接字绑定地址信息
int bind(int sockfd,struct sockaddr *addr,socklen_t len)
sockfd: 创建套接字返回的操作句柄
addr: 要绑定的地址信息
len: 要绑定的地址信息长度
bind可以绑定不同的地址结构,
为了实现统一,因此用户定义地址结构的时候,
定义自己需要的地址结构(例如IPV4就是用struct sockaddr_in),但是进行绑定的时候,
统一类型强转成为sockaddr *类型
bind(fd,struct socketaddr *addr,len) {
if(addr->sa_family == AF_INET) {
// 绑定IPV4地址信息,这个结构体按照sockaddr_in进行解析
}
else if(addr->sa_family == AF_INET6) {
// IPV6地址绑定,按照IPV6地址结构解析
}
else if(addr->sa_family == AF_LOCAL)
{}
- 接收数据 – 不仅仅接收数据,还要通过这个数据得知这个数据是谁发的,以便进行回复
int recvfrom(int sockfd, char* buf, int buf_len, int flag, struct sockaddr* peer_addr, socklen_t* addr_len)
sockfd: socket操作句柄
buf: 一块缓冲区,用于接收从从接受缓冲区中取出的数据
len: 想要接收的数据长度
flag: 操作选项标志,默认为0,表示阻塞操作
peer_addr: 发送方的地址信息
addrlen: 想要获取的地址信息长度以及返回实际长度
返回值: 成功返回接收到的数据字节长度,失败返回-1 - 发送数据
ssize_t sendto(int sockfd, char* data, int data_len, int flag, struct sockaddr* dest_addr, socklen_t addrlen)
sockfd: socket操作句柄
data: 要发送的数据首地址
len: 要发送数据长度
flag: 默认为0,表示阻塞操作
desk_addr: 接收方的地址信息
addrlen: 地址信息长度
返回值: 成功返回实际发送的数据的字节长度,失败返回-1 - 关闭套接字
int close(fd)
网络字节序转换接口
- uint32_t htonl(uint32_t hostlong)
- uint16_t htons(uint16_t hostshort)
- uint32_t ntohl(uint32_t netlong)
- uint16_t ntohs(uint16_t netshort)
- 将字符串的点分十进制IP地址转换为网络字节序的整数IP地址
int_addr_t inet_addr(const char* cp) - 将网络字节序的整数IP地址转换为字符串的点分十进制IP地址
char* inet_ntoa(struct in_addr in) - 将字符串的IP地址转换为网络字节序的整数IP地址
int inet_pton(int af, const char* src, void* dst) - 将网络字节序的整数IP地址转化为字符串的IP地址
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size
在进行uint16_t类型数据转换的时候,不能使用 ntohl / htonl ,只能使用htons / ntohs
UDPsocket.hpp–实现 UDPsocket类
// 使用C++封装一个UDPsocket类
// 实例化出的每一个对象都是一个UDP通信套接字
// 并且通过成员函数实现UDP通信流程
#include<cstdio>
#include<string>
#include<sys/socket.h> // 套接字接口信息
#include<netinet/in.h> // 包含地址结构
#include<arpa/inet.h>
#include<unistd.h>
using namespace std;
class UDPsocket{
public:
// 构造函数
UDPsocket()
:_sockfd(-1)
{}
// 1.创建套接字
bool Socket(){
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(_sockfd < 0){
perror("socket error");
return false;
}
return true;
}
// 2.为套接字绑定地址信息
bool Bind(const string& ip, uint16_t port){
// 定义IPV4地址结构
struct sockaddr_in addr;
addr.sin_family = AF_INET;
// 将主机字节序短整型转换为网络字节序短整型
addr.sin_port = htons(port);
// 将字符串ip地址转换为网络字节序ip地址
addr.sin_addr.s_addr = inet_addr(ip.c_str());
// 绑定
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0){
perror("bind error");
return false;
}
return true;
}
// 3.接收数据并且获取发送端的地址信息
bool Recv(string* buf, string* ip = NULL, uint16_t* port = NULL){
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("recv error");
return false;
}
// 从tmp中截取ret个字节放到buf中
buf->assign(tmp, ret);
if(port != NULL){
// 网络字节序转为主机字节序
*port = ntohs(peer_addr.sin_port);
}
if(ip != NULL){
// 网络字节序到字符串IP地址的转换
*ip = inet_ntoa(peer_addr.sin_addr);
}
return true;
}
// 4.发送数据
bool Send(const string& data, const string& ip, const uint16_t port){
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;
}
// 5.关闭套接字
bool Close(){
if(_sockfd > 0){
close(_sockfd);
_sockfd = -1;
}
return true;
}
private:
// UDP通信套接字描述符
int _sockfd;
};
UDP服务端
#include<iostream>
#include<string>
#include"UDPsocket.hpp"
using namespace std;
#define CHECKRET(q) if((q)==false){return -1;}
int main(int argc, char* argv[]){
// argc表示程序运行参数的个数
if(argc != 3){
cout << "Usage:./UDPsrv IP Port" << endl;
return -1;
}
uint16_t port = stoi(argv[2]);
string ip = argv[1];
UDPsocket srvsock;
// 1.创建套接字
CHECKRET(srvsock.Socket());
// 2.为套接字绑定地址信息
CHECKRET(srvsock.Bind(ip, port));
while(1){
// 3.接收数据
string buf;
string peer_ip;
uint16_t peer_port;
CHECKRET(srvsock.Recv(&buf, &peer_ip, &peer_port));
cout << "client[" << peer_ip << ":" << peer_port << "]say:" << buf << endl;
// 4.发送数据
buf.clear();
cout << "server say: ";
cin >> buf;
CHECKRET(srvsock.Send(buf, peer_ip, peer_port));
}
// 5.关闭套接字
srvsock.Close();
return 0;
}
UDP客户端
#include<iostream>
#include<string>
#include"UDPsocket.hpp"
using namespace std;
#define CHECKRET(q) if((q) == false){return -1;}
int main(int argc, char* argv[]){
// 客户端获取的IP地址是服务端绑定的,也就是客户端发送的目标地址
if(argc != 3){
cout << "Usage: ./UDPcli ip port" << endl;
return -1;
}
string srv_ip = argv[1];
uint16_t srv_port = stoi(argv[2]);
UDPsocket clisock;
// 1.创建套接字
CHECKRET(clisock.Socket());
// 2.为套接字绑定地址信息(不推荐主动绑定)
while(1){
// 3.发送数据
cout << "client say:";
string buf;
cin >> buf;
CHECKRET(clisock.Send(buf, srv_ip, srv_port));
// 4.接收数据
buf.clear();
CHECKRET(clisock.Recv(&buf));
cout << "server say: " << buf << endl;
}
// 5.关闭套接字
clisock.Close();
return 0;
}
TCP通信流程
socket接口
- 创建套接字
int socket(int domain,int type,int protocol)
type: SOCK_DGRAM – 数据报套接字 / SOCK_STREAM – 流式套接字
protocol: IPPROTO_TCP - 绑定地址信息
int bind(int sockfd,struct sockaddr *addr,socklen_t len) - 开始监听
listen(int sockfd,int backlog)
sockfd: 将哪个套接字设置为监听状态。并且监听状态后可以开始接收客户端连接请求
backlog: 同一时间的并发连接数,决定同一时间最多接收多少个客户端的连接请求 - 获取新建连接(从已完成连接队列中取出一个socket,并且返回这个socket的描述符操作句柄)
int accept(int sockfd,struct sockaddr *cli_addr,socklen_t *len)
sockfd: 监听套接字,表示要获取哪个TCP服务端套接字的新建连接
cli_addr: 这个新的套接字对应的客户端地址信息
len: 地址信息长度
返回值: 新建socket套接字的描述符 – 外部程序中的操作句柄 - 接收数据/发送数据
ssize_t recv(int sockfd, char* buf, int len, int flag)
默认阻塞,没有数据则等待,连接断开返回0,不再阻塞
ssize_t send(int sockfd, char* data, int len, int flag)
默认阻塞,缓冲区数据满了则等待,连接断开则触发SIGPIPE异常 - 关闭套接字
int close(fd) - 客户端向服务端发起连接请求
int connect(int sockfd, struct sockaddr* srv_addr, int len)
srv_addr: 服务端地址信息 – 给谁发送连接请求
connect这个接口也会在sockfd的套接字中描述对端地址信息
TCPsocket.hpp–实现 TCPsocket类
#include<cstdio>
#include<string>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
using namespace std;
#define MAX_LISTEN 5
class TCPsocket{
public:
// 构造函数
TCPsocket()
:_sockfd(-1)
{}
// 1.创建套接字
bool Socket(){
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0){
perror("socket error");
return false;
}
return true;
}
// 2.为套接字绑定地址信息
bool Bind(const string& ip, uint16_t port){
// 组织地址结构IPV4
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 = bind(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0){
perror("bind error");
return false;
}
return true;
}
// 3.开始监听
bool Listen(int backlog = MAX_LISTEN){
int ret = listen(_sockfd, backlog);
if(ret < 0){
perror("listen error");
return false;
}
return true;
}
// 4.获取新建连接
bool Accept(TCPsocket* new_sock, string* ip = NULL, uint16_t* port = NULL){
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("accept error");
return false;
}
new_sock->_sockfd = new_fd;
if(ip != NULL){
*ip = inet_ntoa(addr.sin_addr);
}
if(port != NULL){
*port = ntohs(addr.sin_port);
}
return true;
}
// 5.接收数据
bool Recv(string* buf){
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if(ret < 0){
perror("recv error");
return false;
}
else if(ret == 0){
printf("disconnected\n");
return false;
}
buf->assign(tmp, ret);
return true;
}
// 6.发送数据
bool Send(const string& data){
int ret = send(_sockfd, data.c_str(), data.size(), 0);
if(ret < 0){
perror("send error");
return false;
}
return true;
}
// 7.关闭套接字
bool Close(){
if(_sockfd > 0){
close(_sockfd);
_sockfd = -1;
}
return true;
}
// 8.发送连接请求
bool Connect(const string& ip, uint16_t port){
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 = connect(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0){
perror("connect error");
return false;
}
return true;
}
private:
int _sockfd;
};
TCP服务端
#include<iostream>
using namespace std;
#include"TCPsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc, char* argv[]){
if(argc != 3){
cout << "Usage: ./TCPsrv ip port" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
// 创建套接字
TCPsocket listen_sock;
CHECK_RET(listen_sock.Socket());
// 绑定地址信息
CHECK_RET(listen_sock.Bind(ip, port));
// 开始监听
CHECK_RET(listen_sock.Listen());
while(1){
TCPsocket new_sock;
bool ret = listen_sock.Accept(&new_sock);
if(ret == false){
// 服务端不能因为获取一个新建套接字失败就退出
continue;
}
string buf;
new_sock.Recv(&buf);
cout << "client say: " << buf << endl;
buf.clear();
cout << "server say: " << endl;
cin >> buf;
new_sock.Send(buf);
}
listen_sock.Close();
return 0;
}
TCP客户端
#include<iostream>
using namespace std;
#include"TCPsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc, char* argv[]){
if(argc != 3){
cout << "Usage: ./TCPcli ip port" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
TCPsocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Connect(ip, port));
while(1){
string buf;
cout << "client say: " << endl;
cin >> buf;
sock.Send(buf);
buf.clear();
sock.Recv(&buf);
cout << "server say: " << buf << endl;
}
sock.Close();
return 0;
}