網絡套接字編程,主要是針對於傳輸層,因爲傳輸層有兩個協議tcp/udp,因此我們必須選擇其一進行數據傳輸,選哪個那,這種時候我們必須就要明瞭兩個協議的優缺點,視使用場景而定。
TCP協議
優點: 可靠傳輸,並且傳輸靈活
缺點: 傳輸速度低,數據粘包
UDP協議
優點: 傳輸速度快、無粘包
缺點: 不可靠
針對數據安全要求高的場景(文件傳輸)使用TCP保證數據的可靠。
針對數據安全性要求不是很高,但是實時性要求高的場景(視頻傳輸),使用UDP保證傳輸的速度。
socket套接字編程
網絡編程涉及到對網卡的操作,因此操作系統就提供了一套接口來供我們操作——socker接口,網絡編程中分了兩個端:客戶程序端、服務端程序。
網絡編程中,客戶端是主動的一方(永遠是客戶端首先向服務端發起請求),並且客戶端必須知道服務端的地址信息(ip+port),並且服務端必須得在這個指定的地址上等着別人。
socket是一套接口,用於網絡編程的接口,同時socket也是一個數據結構。想要開始網絡編程,就需要先創建一個套接字,也就是說對於我們網絡編程來說,第一步永遠是創建套接字,套接字創建成功後,我們纔可以通過對套接字的操作,來完成網絡上數據的傳輸。
UDP網絡編程
服務端
- 創建套接字
int socket(int domain, int type, int protocol); //創建套接字
domain //地址域
AF_INET //IPE4
type //套接字類型
SOCK_STREAM //流式套接字 tcp
SOCK_DGRAM //數據報套接字 udp
protocol //協議類型
IPPROTO_TCP // tcp協議 6
IPPROTO_UDP //udp協議 17
返回值 //套接字描述符,非負整數
//失敗返回 -1
socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符,應用程序可以像讀寫文件一樣用read/write在網絡上收發數據,如果socket()調用出錯則返回-1。對於IPv4,domain參數指定爲AF_INET。對於TCP協議,type參數指定爲SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type參數指定爲SOCK_DGRAM,表示面向數據報的傳輸協議。
- 爲套接字綁定地址信息
聲明去網卡接受數據時接受的是哪一部分數據(因爲網卡上可能會有很多的數據)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //爲套接字綁定地址信息
sockfd //套接字描述符
addr //地址信息,要綁定的地址信息 構造出IP地址加端口號
addrlen //地址信息長度
返回值 //成功 0
//失敗 -1
//編程中可能會使用的一些函數
unit16_t htons(unit16_t hostshort);
//將一個短整型(16位)數據從主機字節序轉換爲網絡字節序
int addr_t inet_addr(const char *cp)
//將一個點分十進制的字符串ip地址轉化爲網絡字節序
服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號後就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡地址和端口號。
bind()的作用是將參數sockfd和addr綁定在一起,使sockfd這個用於網絡通訊的文件描述符監聽addr所描述的地址和端口號。struct sockaddr *是一個通用指針類型,addr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度
- 接受/發送數據
//接受
ssize_t recvform(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd //套接字描述符,告訴操作系統接受那裏的數據
buf //用於存儲接受的數據
len //想要接受的數據長度
flags //發送標誌 0 默認阻塞
MSG_PEEK //接收數據後數據並不會從緩衝區刪除
//場景:探測性獲取數據
src_addr //發送端的地址信息
addrle //地址信息長度/實際獲取地址信息長度
返回值 //實際讀取到的數據字節長度 失敗 -1
//發送
ssize_t sendto(int sockfd, const void *buf, size_t len, int flag, struct sockaddr *dest_addr, socklen_t *addrlen);
sockfd //套接字描述符,發送數據的時候是通過這個socket所綁定的地址來發送
buf //要發送的數據
len //要發送的數據長度
flag //發送標誌 0 默認阻塞
MSG_PEEK //接收數據後數據並不會從緩衝區刪除
//場景:探測性獲取數據
dest_addr //數據要發送到的對端地址信息
addrle //地址信息長度
返回值 //實際發送到的數據字節長度 失敗 -1
- 關閉套接字
close(int sockfd);
客戶端
客戶端與服務端在套接字編程中所使用的函數相同
- 創建套接字
- 爲套接字綁定地址信息
- 接受/發送數據
- 關閉套接字
模擬實現UDP客戶端與服務端
//服務端
int main()
{
//創建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0)
{
printf(error);
return - 1;
}
//爲套接字綁定地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = 9000; //htons(9000)
addr.sin_addr.s_addr = inet_addr(""); //ip地址,字符串
//inet_pton(AF_INET, "", &addr.sin_addr.s_addr);
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(sockfd, (struct sockaddr*)&addr, len); //綁定不一定成功
if (ret < 0)
{
perror(); //自動恢復資源
close(sockfd);
return -1;
}
//接受數據
while (1)
{
char buff[1024] = { 0 };
struct sockaddr_in cli_addr;
len = sizeof(struct sockaddr_in);
ssize_t rlen = recvfrom(sockfd, buff, 1023, 0, (struct sockaddr*)&cli_addr, &len);
if (rlen < 0)
{
perror();
close(sockfd);
return -1;
}
printf("client[%s:%d] say:%s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buff); //打印一下客戶端信息
//發送消息
memset(buff, 0x00, 1024);
scanf("%s", buff);
sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&cli_addr, len);
}
close(sockfd);
return 0;
}
//客戶端
int main()
{
//1.
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd < 0)
{
printf(error);
close(sockfd);
return -1;
}
//2.
//需要定義服務端的地址信息
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9000);
serv_addr.sin_addr.s_addr = inet_addr(""); //ip地址,字符串
//inet_pton(AF_INET, "", &addr.sin_addr.s_addr);
socklen_t len = sizeof(struct sockaddr_in);
//3.4.
while (1)
{
char buff[1024] = { 0 };
scanf("%s", buff);
sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&serv_addr, len); //客戶端 與 服務端只有一個
memset(buff, 0x00, 1024);
ssize_t r_len = recvfrom(sockfd, buff, 1023, 0, (struct sockaddr*)&serv_addr, &len);
if (r_len < 0)
{
perror();
close(sockfd);
return -1;
}
printf(buff);
}
close(sockfd);
return 0;
}
TCP網絡套接字編程
服務端
服務端在創建套接字與爲套接字綁定地址信息、關閉套接字時所使用的函數與UDP編程相同
- 創建套接字
- 爲套接字綁定地址信息
- 開始監聽
int listen(int sockefd, int backlog);
sockefd //套接字描述符
backlog //一個整形數字 用以定義一個掛起的連接隊列最大結點數,表示同一時間的一個併發連接數(同一時間能夠接受多少個新客戶端連接)
//定義已完成連接隊列的最大結點數
//每一個客戶端都會創建新的socket,一個新連接建立連接有一個過程,如果這個新的連接已經完成三次握手過程,就將這個新的socket放到這個隊列中
//這個backlog決定了同一時間的最大併發連接數
返回值 //失敗:返回-1
- 獲取新創建的socket
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd //套接字描述符
addr //進連接的客戶端地址信息
addrlen //用於確定要獲取地址信息的長度,接受實際長度
//輸入輸出複合型參數,
//傳出參數,返回鏈接客戶端地址信息,含IP地址和端口號
返回值 //新建的socket連接的套接字描述符,失敗:-1
三方握手完成後,服務器調用accept()接受連接,如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。addr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數(value-result argument),傳入的是調用者提供的緩衝區addr的長度以避免緩衝區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有佔滿調用者提供的緩衝區)。如果給addr參數傳NULL,表示不關心客戶端的地址。
accept的理解
- 每一個客戶端向服務端發起連接請求,在服務端都會新建一個socket結構,當這個連接走完三次握手過程,完成建立連接,這個新的socket結構會放到已完成連接隊列中
- accept獲取新連接的客戶端,實際上是從已完成連接隊列獲取一個已完成連接的socket,並且返回這個新的socket的套接字描述符
- accept返回的套接字是用來與客戶端進行通信的套接字
- 接受/發送數據
//發送數據
ssizet_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd //套接字描述符
buf //發送的數據
len //發送數據的長度
flags //0 默認阻塞
返回值 //實際發送的數據長度, 失敗 -1
//接受數據
ssizet_t recv(int sockfd, const void *buf, size_t len, int flags);
sockfd //套接字描述符
buf //接受的數據
len //接受數據的長度
flags //0 默認阻塞
返回值 //>0 實際接受的數據長度
//==0 鏈接斷開
//-1 出錯
- 關閉套接字
客戶端
客戶端在TCP套接字編程中使用的函數多數與服務端相同
- 創建套接字
- 向服務端發起鏈接請求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd //套接字描述符
addr //服務端地址信息 傳入參數,指定服務器端地址信息,含IP地址和端口號
addrlen //地址信息長度
返回值 //失敗返回-1,成功0
- 接收/發送數據
- 關閉套接字
模擬實現TCP客戶端與服務端
#define CHECK_RET(q) if((q) == false){return false;}
class TcpSocket
{
private:
int _sockfd;
//int port;
//std::string ip;
public:
//初始弧
//構造函數,初始化port與ip、socket(-1)
//析構函數
//1.創建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0)
{
perror();
//std::cerr<<""<<std::endl;
return false;
}
}
//2.綁定地址信息
bool Bind(std::string &ip, uint16_t port)
{
sockaddr_in addr;
//bzreo(&addr, sizeof(addr)); //清0
addr.sin_family = AF_INET;
addr.sin_port = htonl(port); //htonl(INADDR_ANY)
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(sockaddr_in);
int ret = bind(_sockfd, (sockaddr*)&addr, len);
if (ret < 0)
{
perror(); //自動恢復資源
return false;
}
return true;
}
//3.監聽
bool Listen(int backlog = 5)
{
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
perror();
return false;
}
return true;
}
//3.發起鏈接請求
bool Connect(std::string &ip, uint16_t port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htonl(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(sockaddr_in);
int ret = connect(_sockfd, (sockaddr*)&addr, len);
if (ret < 0)
{
perror();
return false;
}
return true;
}
//運行
//4.獲取新建的socket鏈接
bool Accept(TcpSocket *sock, std::string *ip, uint16_t *port)
{
int newfd;
sockaddr_in addr;
socklen_t len = sizeof(sockaddr_in);
newfd = accept(_sockfd, (sockaddr*)&addr, &len);
if (newfd < 0)
{
perror();
return false;
}
sock->_sockfd = newfd;
return true;
}
//5.發送/接受
bool Send(char *buf, size_t len)
{
int slen = 0;
while (slen < len)
{
int ret = send(_sockfd, buf + slen, len - slen, 0);//未發完繼續發
if (ret < 0)
{
return false;
}
slen += len;
}
return true;
}
bool Recv(char* buf, size_t *len = NULL)
{
int ret rlen = 0;
if (len)
{
while (rlen < *len)
{
ret = recv(_sockfd, buf + rlen, *len - rlen, 0);
if (ret < 0)
{
if (errno == EAGAIN || errno == EINTR) //EAGAIN:緩衝區沒有數據 EINTR:接受數據的過程被信號大端
{
continue;
}
return false;
}
else if (ret == 0)
{
printf();
return false;
}
rlen += ret;
}
}
else
{
ret = recv(_sockfd, buf, 1024, 0);
if (*len < 0)
{
return false;
}
if (len)
*len = ret;
}
return true;
}
//關閉套接字
bool Close()
{
close(_sockfd);
return true;
}
};
//服務端
//sock是專門用於獲取客戶端新連接的socket,稱之爲監聽socket
//client是客戶端新建的socket,專門用於跟客戶端進行數據傳輸
int main()
{
if (argc != 3)
{
printf();
return -1;
}
str::string ip = argv[1];
unit16_t port = atoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket()); //1
CHECK_RET(sock.Bind(ip, port)); //2
CHECK_RET(sock.Listen()); //3
while (1)
{
TcpSocket client;
std::string ip;
unint16_t port;
if (sock.Accept(&client, &ip, &port) == false)
{
continue;
}
client.Recv(buff);
printf();
memset(buff, 0x00, 1024);
fflush(stdout);
scanf();
client.Send(buff, strlen(buff));
}
sock.Close();
return 0;
}
//客戶端
int main()
{
if (argc != 3)
{
printf();
return -1;
}
str::string ip = argv[1];
unit16_t port = atoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket()); //1
CHECK_RET(sock.Connect(ip,port));//2
while (1)
{
char buff[1024] = { 0 };
fflush(stdout);
scanf();
sock.Send(buff, strlen(buff));
memset(buff, 0x00, 1024);
sock.Recv(buff);
printf();
}
sock.Close();
return 0;
}
tcp斷開連接檢測:
對於接受端來說,recv返回值如果是0,就代表斷開了。
對於發送端來說,send會觸發Broken pipe異常,接受SIGPIPE信號,導致程序退出(對於大多是程序來說,連接斷開了,不應該退出程序,而是重新連接,所以需要對SIGPIPE信號自定義處理方式)。