文章目錄
TCP網絡編程
server(服務器代碼的編寫)
-
1.初始化
-
2.進入一個主循環(死循環)
a.讀取客戶端發來的"請求"(Request)
b.根據請求內容,計算生成"響應"(Respnose)內容最核心
c.把響應數據返回給客戶端
socket():創建套接字
調用格式
- 1.使用socket()函數創建一個文件描述符
int socket(AF_INET,SOCK_DGRAM,0);
//參數一:表示改文件描述符使用的是IPV4
//參數二:表示該文件描述符使用的是UDP協議
//參數三:一般不使用
-
參數domain 用來說明網路程序所在的主機採用的是那種通信協議簇,這些協議族再頭文件<sys/socket.h>中定義
-
AF_INET //表示IPv4網絡協議
-
AF_INET6 //IPv6網絡協議
-
參數type 用來指明創建的套接字類型,
-
SOCK_STREAM:流式套接字,面向連接可靠的通信類型
-
SOCK_DGRAM:數據報套接字,非面向連接和不可靠的通信類型
-
SOCK_RAW:原始套接字,用來直接訪問IP協議
-
參數protocol 指定套接字使用的協議,一般採用默認值0,表示讓系統根據地址格式和套接字類型,自動選擇一個合適的協議
-
返回值:
-
調用成功就創建了一個新的套接字並返回它的描述符,在之後對該套接字的操作中都要藉助這個文件描述符,
-
否則返回-1(<0) 表示套接字出錯.應用程序可調用WSAGetLastError() 獲取相應的錯誤代碼
bind ():將套接字綁定到指定的網絡地址
使用這個函數前需要將服務器的ip和端口號賦值到結構體sockaddr_in中
使用bind()函數前的準備工作
- sockaddr 結構:針對各種通信域的套接字,存儲它們的地址信息
struct sockaddr{
u_short sa_family; /*16位協議家族*/
char sa_data[14] /*14字節協議地址*/
};
- sockaddr_in結構:專門針對Internet通信域,存儲套接字相關的網絡地址信息(IP地址、傳輸層端口號等信息)
struct sockadd_in{
short int sin_family; //地址家族
unsigned short int sin_port; //端口號
struct in_addr sin_addr; //IP地址
unsigned char sin_zero[8]; //全爲0
};
- in_addr 結構:專門用來存儲IP地址
struct in_addr{
unsigned long s_addr;
};
#include<netinet/in.h>
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
-
參數sockfd:是未經綁定的套接字文件描述符,是由socket()函數返回的,要將它綁定到指定的網絡地址上
-
參數addr : 是一個指向sockaddr結構變量的指針,所指結構體中保存着特定的網絡地址,就是要把套接字sockfd綁定到這個地址上.
-
參數 addrlen :是結構sockaddr結構的長度,等於sizeof(struct sockadd
-
返回值
返回0:表示已經正確的實現了綁定
如果返回SOCKET_ERROR表示有錯。應用程序可調WSAGetLastError()獲取相應的錯誤代碼
- 最後在函數調用的時候,將這個結構強制轉換成sockaddr類型
本機字節序和網絡字節序
-
本機字節序在不同的計算機中,存放多字節值的順序是不一樣的,有的是從低到高,有的是從高到低.計算機中的多字節數據的存儲順序稱爲本機字節順序.
-
網絡字節序:在網絡協議中,對多字節數據的存儲,有它自己的規定。多字節數據在網絡協議報頭中的存儲順序,稱爲網絡字節序.在套接字中必須使用網絡字節序
所以把IP地址和端口號裝入套接字時,我們需要將本機字節序抓換成網絡字節序,在本機輸出時,我們需要將它們從網絡字節序轉換成本機字節序
-
套接字編程接口專門爲解決這個問題設置了4個函數
-
1.htons():短整數本機順序轉換爲網絡順序,用於端口號
-
2.htonl():長整數本機順序轉換成網絡順序,用於IP地址
-
3.ntohs():短整數網絡順序轉換爲本機順序,用於端口號
-
4.ntohl():長整數網絡順序轉換爲本機順序,用於IP地址
這4個函數將被轉換的數值作爲函數的參數,函數返回值作爲轉換後的結果
- 點分十進制的IP地址的轉換
在Internet中,IP地址常常是用點分十進制的表示方法,但在套接字中,IP地址是無符號的長整型數,套接字編程接口設置了兩個函數,專門用來兩種形式IP地址的轉換
- 1.inet_addr函數
unsigned long inet_addr(const char* cp);
/*
入口參cp:點分十進制形式的IP地址
返回值:網絡字節順序的IP地址,是無符號的長整數
*/
- 2.inet_ntoa函數
char* inet_ntoa(struct in_addr in)
/*
入口參數in: 包含長整數IP地址的in_addr結構變量
返回值:指向點分十進制IP地址的字符串的指針
*/
listen():啓動服務器,監聽客戶機端的連接請求
調用格式
int listen(SOCKET s,int backlog);
-
參數s:服務器端的套接字描述符,一般先進行綁定到熟知的服務器端口,要通過他監聽來自客戶端的連接請求,一般將這個套接字稱爲監聽套接字
-
參數backlog:指定監聽套接字的等待連接緩衝區隊列的最大長度,一般爲5
-
返回值:正確執行則返回0出錯返回SOCKET_ERROR
-
函數功能:本函數適用於支持連接的套接字,在Internet通信域,僅用於流式套接字,並僅用於服務器端
accept():接收連接請求
調用格式
SOCKET accept(SOCKET s,struct sockaddr* addr,int * addrlen);
-
參數s:服務器端監聽套接口描述符,調用listen()後,該套接口一直在監聽連接
-
參數addr:可選參數,指向sockaddr結構的指針,該結構用來接收下面通信層所知的請求連接以方的套接字的網絡地址
-
一個出口參數,用來但會下面通信層所指的對方連接實體的網絡地址。
-
addr參數的實際格式由套接字創建時所產生的地址家族確定。
-
-
參數addrlen:可選參數,指向整形數的指針.用來返回addr地址的長度
- addrlen參數也是一個出口參數,在調用時初始化爲addr所指的地址長度,在調用結束時它包含了實際返回的地址的字節長度,如果addr與addrlen中有一個爲nullptr,將不返回所接收的遠程套幾口的任何地址信息.
-
返回值: 如果正確執行,則返回一個SOCKET類型的文件描述符,否則,返回INVALID_SOCKET錯誤,應用程序可通過調用WSAGetError()來獲取特定的錯誤代碼
-
函數功能: 本函數從監聽套接字s的等待隊列中抽取第一個連接請求,創建一個與s同類的新的套接口,來與請求連接的客戶套接字創建連接通道,如果連接成功,就返回新創建的套接字描述符,並且監聽套接字採用阻塞工作方式,則accept()阻塞調用它的進程,直到新的連接請求出現.
recv():從一個已經連接套接口接收數據
調用格式
int recv(SOCKET s,char* buf,int len,int flag);
-
參數s: 套接字描述符,表示一個接口端已經與對端建立連接的套接口
-
參數buf: 用於接收數據的字符緩衝區指針,這個緩衝區是用戶進程的接收緩衝區
-
參數len: 用戶緩衝區長度,以字節大小 計算
-
參數flag: 指定函數的調用方式,一般設置爲0
-
返回值: 如果執行正確,返回從套接字s實際讀入到buf中的字節數,如果連接已終止,返回0;否則的話,返回SOCKET_ERROR錯誤
-
recv()函數功能: s是接收端,既調用本函數一方所創建的本地套接字,可以是數據報套接字或者流式套接字,它已經與對方建立了TCP連接,該套接字的數據接收緩衝區中存有對方發送來的數據,調用recv()函數就是將本地的套接字數據接收緩衝區中的數據接收到用戶進程的緩衝區中
send():向一個已連接的套接口發送數據
調用格式
int send(SOCKET s,char* buf,int len,int flags);
-
參數s : SOCKET 描述符,標識發送方已與對方建立連接的套接口,就是要藉助連接從這個套接口 發送數據
-
參數buf:指向用戶進程的字符緩衝區的指針,該緩衝區包含要發送的數據
-
參數len:用戶緩衝區的數據的長度,以字節計算
-
參數flags:執行次調用的方式,此參數一般置爲0
-
返回值: 如果執行正確,返回實際發送出去的數據的字節總數,要注意這個數字可能小於len中規定的大小;否則,返回SOCKET_ERROR
-
send()函數的調用功能:send()函數用於向本地已建立連接 的數據報或流式套接口發送數據,不論是客戶機還是服務器應用程序都用send漢納樹向TCP連接的另一端發送數據.客戶端程序一般用send()函數向服務器發送請求,服務器則用send()函數向客戶機程序發送應答
實例代碼
tcp_socket.hpp
#pragma once
#include<cstdio>
#include<cstring>
#include<string>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
class TcpSocket{
public:
TcpSocket()
:_fd(-1)
{
}
bool Socket(){
//和UDP 不同的是,第二個參數面向字節流(TCP)
_fd=socket(AF_INET,SOCK_STREAM,0);
if(_fd<0){
perror("socket");
return false;
}
return true;
}
//給服務器使用
bool Bind(const std::string&ip,uint16_t port){
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(ip.c_str());
addr.sin_port=htons(port);
int ret=bind(_fd,(sockaddr*) &addr,sizeof(addr));
if(ret<0){
perror("bind");
return false;
}
return true;
}
//給服務器使用
bool Listen(){
//listen 進入監聽狀態
//所謂的" 連接" 指的是一個五元組
//源ip,源端口,目的端口,協議
int ret=listen(_fd,10);
if(ret<0){
perror("listen");
return false;
}
return true;
}
//給服務器使用
bool Accept(TcpSocket* peer,std::string* ip=NULL,uint16_t* port =NULL){
//accept 從連接隊列中取一個連接到用戶代碼中
//如果隊列中沒有連接,就會阻塞(默認行爲)
sockaddr_in peer_addr;
socklen_t len=sizeof(peer_addr);
//返回值也是一個 socket
int client_sock=accept(_fd,(sockaddr*)&peer_addr,&len);
if(client_sock<0){
perror("accept");
return false;
}
peer->_fd=client_sock;
if(ip!=NULL){
*ip = inet_ntoa(peer_addr.sin_addr);
//把peer_addr所包含的IP地址轉換成點分十進制
//交給用戶
}
if(port!=NULL){
*port=ntohs(peer_addr.sin_port);
//把網絡序轉換成主機序
}
return true;
}
//客戶端和服務器都會使用
bool Recv(std::string *msg) {
//括號後面的const 修飾this指針
msg->clear();
char buf[1024*10]={0};
ssize_t n=recv(_fd,buf,sizeof(buf)-1,0);
//recv的返回值,如果讀取成功,返回結果爲讀到的字節數
//如果讀取失敗,返回結果爲-1
//如果對端關閉了 socket 返回結果爲 0
if(n<0){
perror("recv");
return -1;
}else if(n==0){
//需要考慮返回0的情況
return 0;
}
msg->assign(buf);
return true;
}
//客戶端和服務器都會使用
bool Send(const std::string& msg) {
ssize_t n=send(_fd,msg.c_str(),msg.size(),0);
if(n<0){
perror("send");
return false;
}
return true;
}
//給客戶端使用
bool Connect(const std::string &ip,uint16_t port){
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(ip.c_str());
addr.sin_port=htons(port);
int ret=connect(_fd,(sockaddr*)&addr,sizeof(addr));
if(ret<0){
perror("connect");
return false;
}
return true;
}
bool Close(){
if(_fd!=-1){
close(_fd);
}
return true;
}
private:
int _fd;
};
tcp_server.hpp
#pragma once
//通用的TCP 服務器
#include"tcp_socket.hpp"
#include<functional>
#define CHECK_RET(exp) if(!(exp)){\
return false;\
}
typedef std::function<void(const std::string&,std::string*)> Handler;
class TcpServer{
public:
TcpServer(){
}
bool Start(const std::string& ip,uint16_t port,
Handler handler){
//tcp 啓動的基本流程
//1.先創建一個socket
CHECK_RET(listen_sock_.Socket());
//2.綁定端口號
CHECK_RET(listen_sock_.Bind(ip,port));
//3.進行監聽
CHECK_RET(listen_sock_.Listen());
printf("Server start OK\n");
//4.進入主循環
while(true){
//5.通過accept 獲取到一個連接
TcpSocket client_sock; //和客戶端溝通的socket
std::string ip;
uint16_t port;
//核心問題在於,第一次Accept 之後就進入了一個循環
//在這個操作過程中,循環一直沒有結束,Accept 沒有被重複調用到
//後續鏈接過來的客戶端都在內核中的鏈接隊列中排隊呢,一直得不到處理
//應該想辦法讓我們的程序能夠更快速的調用到Accept
//多進程或者多線程解決
bool ret=listen_sock_.Accept(&client_sock,&ip,&port);
if(!ret){
continue;
}
printf("[%s:%d] 有客戶端連接!\n",ip.c_str(),port );
//6.和客戶端進行具體的溝通,一次連接
//就進行多次更新
while(true){
//1.讀取請求
std::string req;
int r=client_sock.Recv(&req);
if(r<0){
continue;
}
if(r==0){
//對端關閉了 socket
client_sock.Close();
printf("[%s:%d] 對端關閉了連接\n",ip.c_str(),port);
break;
}
printf("[%s:%d] 對客戶端發送了:%s\n",ip.c_str(),port,req.c_str());
//2.根據請求計算響應
std::string resp;
handler(req,&resp);
//3.把響應寫回到客戶端
client_sock.Send(resp);
}
}
}
private:
TcpSocket listen_sock_;
};
tcp_client.hpp
#pragma once
#include"tcp_socket.hpp"
//給用戶提供的信息越少越好
class TcpClient{
public:
TcpClient(){
_sock.Socket();
}
~TcpClient(){
_sock.Close();
}
bool Connect(const std::string &ip,uint16_t port){
return _sock.Connect(ip,port);
}
int Recv(std::string *msg){
return _sock.Recv(msg);
}
bool Send(const std::string&msg){
return _sock.Send(msg);
}
private:
TcpSocket _sock;
};