前言
套接字编程其实就是网络编程,套接字实际就是一套网络通信程序编写的接口,通过这些接口,并且提供相关信息,就可以实现传输层以下几层的操作。
网络通信中涉及两台主机之间的通信:客户端(主动发送请求)、服务端(被动接收请求)。
一:TCP/UDP协议的基本认识
在TCP/IP网络体系结构中,TCP协议和UDP协议是传输层两种典型的协议,为上层用户提供级别的通信可靠性。
1.1 TCP:传输控制协议(Transport Control Protocol)
传输特点: 有连接、可靠传输、面向字节流
TCP通信需要建立连接(打电话),确保数据被对方收到,有序的安全的字节流传输服务
应用场景: 数据传输安全性要求高(文件传输)
1.2 UDP:用户数据报协议(User Data Protocol)
传输特点: 无连接、不可靠、面向数据报
UDP通信不需要建立连接(发短信),不确保数据是否被对方收到,无序的不可靠的数据块传输
应用场景: 数据传输实时性要求高(视频传输 )
二:UDP通信流程
注意:客户端用哪个源端地址信息发送数据不影响大局,只要服务端会回复到客户端绑定的源端地址信息就可以。(客户端一旦自己绑定源端地址信息,若选用的端口已经被占用则会绑定失败,所以不推荐客户端主动绑定源端地址信息)
2.1 UDP套接字相关接口
- 创建套接字
int socket(int domain, int type, int protocal)
返回一个非负整数(找到socket结构体)
domain:地址域
不同的协议版本有不同的地址结构——IP地址结构:IPv4(AF_INET)、IPv6(AF_INET6)
确定socket通信使用哪种协议版本的地址结构
type:套接字类型
数据报套接字: SOCK_DGRAM提供数据报传输服务(无连接、不可靠、有最大长度限制的消息传输服务)
流式套接字: SOCK_STREAM提供字节流传输服务 (有序、可靠、基于连接的消息传输服务)
protocal:协议类型
数据报套接字:默认UDP协议(IPPROTO_UDP)
流式套接字:默认TCP协议(IPPROTO_TCP)
- 为套接字绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)
sockfd:创建套接字返回的描述符
addr: 地址信息的结构(绑定各种各样的地址信息)
bind可以绑定不同的地址结构,为了实现接口统一,用户定义时定义自己需要的地址结构,绑定时统一将类型强转为sockaddr*
addrlen: 地址信息的长度
- 接收数据
int recvfrom(int sockfd, char* buf, int buf_len, int flag, struct sockaddr* peer_addr, socklen_t* addr_len)
sockfd:指定内核中的socket结构体(从哪个socket的接收缓冲区中取出数据)
buf:用户态缓冲区,存放从接收缓冲区中取出的数据
buflen:想要获取的数据长度
flag:操作选项,默认为0阻塞接收(缓冲区中没有数据则阻塞等待)
peer_addr:地址缓冲区首地址,获取发送这个数据的源端地址信息
addr_len:指定想要获取地址信息的长度以及返回实际获取的长度
- 发送数据
ssize_t sendto(int sockfd, char* data, int data_len, int flag, struct sockaddr* addr dest_addr, socklen_t addrlen)
sockfd:指定内核中socket结构体,绑定的地址信息做为数据中的源信息对数据进行描述
data/datalen:要发送的数据以及数据长度
flag:默认为0阻塞发送数据,若发送缓冲区中数据饱和则进行等待
dest_addr:目的端的地址信息
addr_len:地址信息长度
- 关闭套接字
int close(int fd)
2.2 UDP套接字代码实现
- 主机字节序到网络字节序的转换接口(端口):
32位整数:uint32_t htonl(uint32_t hostlong)
16位整数:uint16_t htons(uint16_t hostshort)
- 网络字节序到主机字节序的转换接口(端口):
32位整数:uint32_t ntohl(uint32_t netlong)
16位整数: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)
UDPsocket.hpp:C++封装一个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;
};
UDPsrv.cc: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;
}
UDPcli.cc: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通信流程
注意:TCP通信需要建立连接,监听套接字在收到客户端的连接请求后,才会创建通信套接字用于指定客户端和服务端的通信,通信套接字同时包含源端地址信息和对端地址信息,收发数据没有固定的先后顺序。
3.1 TCP套接字相关接口
- 创建套接字
接口与UDP通信相同,SOCK_STREAM为流式套接字,默认TCP协议
- 为套接字绑定地址信息
接口与UDP通信相同
- 开始监听
listen(int sockfd, int backlog)
sockfd:套接字描述符(设置此套接字为监听状态,并且开始接收客户端的连接请求)
backlog:同一时间的并发连接数
- 获取新建连接
从已完成连接的套接字队列中取出一个socket,并且返回这个socket的描述符
int accept(int sockfd, struct sockaddr* cli_addr, socklen_t len)
sockfd:监听套接字描述符(获取哪个服务端套接字的新建连接)
cli_addr / len:新建套接字对应的客户端地址信息以及地址信息长度
返回值:新建套接字的套接字描述符
- 收发数据
TCP通信套接字中已经包含了源端地址信息和对端地址信息,所以在接收数据的时候不需要获取对方的地址信息,发送数据的时候也不需要指定对端地址信息。
ssize_t recv(int sockfd, char* buf, int len, int flag)
:默认阻塞,缓冲区没有数据则等待,连接断开返回0
ssize_t send(int sockfd, char* data, int len, int flag)
:默认阻塞,缓冲区满则等待,连接断开触发SIGPIPE异常
- 关闭套接字
close(fd)
- 客户端向服务端发起连接请求
int connect(int sockfd, struct sockaddr* srv_addr, int len)
srv_addr:服务端地址信息
connect也会在套接字socket中描述对端地址信息
3.2 TCP套接字代码实现
TCPsocket.hpp:C++封装一个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;
};
TCPsrv.cc: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;
}
TCPcli.cc: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;
}
我们很容易发现一个问题:
本次实现的TCP通信流程只能完成一个客户端与服务端的一次通信,因为服务端的开始监听和收发数据放在同一个while死循环中,数据一次的收发结束,就会回到监听阶段,阻塞等待新连接的到来,从而只能完成一个客户端与服务端的一次通信。
解决办法:
父进程用于监听操作,阻塞等待新连接的到来
子进程用于指定客户端与服务端的通信,父子进程互不影响。
注意:
子进程退出时,父进程需要循环非阻塞等待子进程的退出,避免产生僵尸进程。
并且这里还需要处理SIGCHILD信号(不可靠信号)为自定义,直到有子进程退出时才循环非阻塞处理,在一次处理中必须处理到没有子进程退出才可以,避免信号的丢失。