前言
套接字編程其實就是網絡編程,套接字實際就是一套網絡通信程序編寫的接口,通過這些接口,並且提供相關信息,就可以實現傳輸層以下幾層的操作。
網絡通信中涉及兩臺主機之間的通信:客戶端(主動發送請求)、服務端(被動接收請求)。
一: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信號(不可靠信號)爲自定義,直到有子進程退出時才循環非阻塞處理,在一次處理中必須處理到沒有子進程退出纔可以,避免信號的丟失。