c++ 實時通信系統(c++socket篇)


  在上一篇簡單的介紹了TCP/IP協議,在這一篇我們主要介紹socket的具體實現的函數

第一步首先我們套添加上頭文件:(#pragma comment(lib, "WS2_32")這是靜態的加入庫文件,這里面有API函數的內容)

#include <winsock2.h>  
#include<ws2tcpip.h>//定義socklen_t

using namespace std;

#pragma comment(lib, "WS2_32")  // 鏈接到WS2_32.lib

第二步初始化套接字環境,(為了在應用程序當中調用任何一個Winsock API函數,首先第一件事情就是必須通過WSAStartup函數完成對Winsock服務的初始化,因此需要調用WSAStartup函數。使用Socket的程序在使用Socket之前必須調用WSAStartup函數。該函數的第一個參數指明程序請求使用的Socket版本,)

	int   Ret;
	WSADATA   wsaData;                        // 用於初始化套接字環境
	// 初始化WinSock環境
	if ((Ret = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0)
		{
		   printf("WSAStartup()   failed   with   error   %d\n", Ret);
		   WSACleanup();

		}

第三步接下來就是socket的API函數的按順序調用就是上一篇中的socket架構,接下來介紹一下API函數

 1.socket

  int socket(int domain, int type, int protocol);

SOCKET s = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if (s == INVALID_SOCKET)
    {
        int er = WSAGetLastError();
        return 0;
    }

  

  socket函數對應於普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用於創建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣.

  創建socket的時候,也可以指定不同的參數創建不同的socket描述符,socket函數的三個參數分別為:

  • domain:即協議域,又稱為協議族(family)。常用的協議族有,AF_INET(其實就是IPV4)、AF_INET6(IPV6)、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
  • type:指定socket類型。常用的socket類型有,SOCK_STREAM(流套接字,使用TCP協議)、SOCK_DGRAM(數據報套接字,使用UDP協議)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的類型有哪些?)。
  • protocol:故名思意,就是指定協議。常用的協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議(這個協議我將會單獨開篇討論!)。

注意:並不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的默認協議。

當我們調用socket創建一個socket時,返回的socket描述字它存在於協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind()函數,否則就當調用connect()、listen()時系統會自動隨機分配一個端口。我們創建的套接字都是主動的,阻塞的套接字。

  如果不出錯,socket函數將返回socket的描述符(句柄,一般都是大於零的),否則,將返回INVALID_SOCKET。

  2.bind

  int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockaddr_in service;
  service.sin_family = AF_INET;
  service.sin_addr.s_addr = inet_addr("127.0.0.1");
  service.sin_port = htons(27015);

  //----------------------
  // Bind the socket.
  if (bind( ListenSocket, (SOCKADDR*) &service,sizeof(service)) == SOCKET_ERROR) {
    closesocket(ListenSocket);
    return;
  }

  

  正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。

  函數的三個參數分別為:

  • sockfd:即socket描述字,它是通過socket()函數創建了,唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。
  • addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址創建socket時的地址協議族的不同而不同,如ipv4對應的是: 
    struct sockaddr_in {
        sa_family_t     sin_family; /* address family: AF_INET */
        in_port_t       sin_port;   /* port in network byte order */
        struct in_addr  sin_addr;   /* internet address */
    };
    
    /* Internet address. */
    struct in_addr {
        uint32_t       s_addr;     /* address in network byte order */
    };
    //ipv6對應的是: 
    struct sockaddr_in6 { 
        sa_family_t     sin6_family;   /* AF_INET6 */ 
        in_port_t       sin6_port;     /* port number */ 
        uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
        struct in6_addr sin6_addr;     /* IPv6 address */ 
        uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
    };
    
    struct in6_addr { 
        unsigned char   s6_addr[16];   /* IPv6 address */ 
    };
    //Unix域對應的是: 
    #define UNIX_PATH_MAX    108
    
    struct sockaddr_un { 
        sa_family_t sun_family;               /* AF_UNIX */ 
        char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
    };
  • addrlen:對應的是地址的長度。

通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用於提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是為什么通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。

  無錯誤返回0,又錯誤返回SOCKET_ERROR(-1),我們可以調用WSAGetLastError來看具體的錯誤。

htons(host to unsigned short)和htonl(host to unsigned long)

各個機器cpu對數據存儲和表示的方法不通,intel機器用littele-endian存數據,而IBM機器用big-endian存數據。網絡協議為取消這種差異,一致采用big-endian方式。htons用於將unsigned short的數值從littele-endian轉換為big-endian,由於short只有2字節,常用語port數值的轉換。htons用於將unsigned long的數值從littele-endian轉換為big-endian,long有4字節,常用於ipv4地址的轉換。

1 u_short htons( __in     u_short hostshort);
2 u_long htonl(__in      u_long hostlong);

 

inet_addr和inet_ntoa

inet_addr用於將ipv4格式的字符串轉換為unsigned long的數值。inet_ntoa用於將struct in_addr的地址轉換為ipv4格式的字符串。

1 unsigned long inet_addr(  __in    const char* cp);
2 char* FAR inet_ntoa(  __in     struct   in_addr in);

  3.listen

  int listen(int sockfd, int backlog);

if (listen(s,SOMAXCONN ) == SOCKET_ERROR)
   {
          int er = WSAGetLastError();
          closesocket(s);
      }

  listen函數的第一個參數即為要監聽的socket描述字,第二個參數為相應socket可以排隊的最大連接個數。socket()函數創建的socket默認是一個主動類型的,listen函數將socket變為被動類型的,等待客戶的連接請求。

  沒有錯誤發生將返回0,否則返回SOCKET_ERROR .

      4.connect

  int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockaddr_in server;
        server.sin_family = AF_INET;
        server.sin_port = htons(8828);
        server.sin_addr.s_addr = inet_addr("127.0.0.1");
        if (connect(cnetsocket,(sockaddr*)&server,sizeof(server)) == SOCKET_ERROR)
        {
            break;
        }

  connect函數的第一個參數即為客戶端的socket描述字,第二參數為服務器的socket地址,第三個參數為socket地址的長度。客戶端通過調用connect函數來建立與TCP服務器的連接。

  0表示正確,否則,將返回SOCKET_ERROR。如果是阻塞式的socket連接,返回值代表了連接正常與失敗。

  5.accept

  int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

SOCKET ac =accept(listener, (struct sockaddr*)&client_address, &client_addrLength);
    if (ac == INVALID_SOCKET)
    {
        int er = WSAGetLastError();
        closesocket(s);
    }

  

accept函數的第一個參數為服務器的socket描述字,第二個參數為指向struct sockaddr *的指針,用於返回客戶端的協議地址,第三個參數為協議地址的長度。如果accpet成功,那么其返回值是由內核自動生成的一個全新的描述字,代表與返回客戶的TCP連接。

如果不發生錯誤,accept將返回一個新的SOCKET描述符,即新建連接的socket句柄。否則,將返回INVALID_SOCKET。傳進去的addrlen應該是參數addr的長度,返回的addrlen是實際長度

注意:accept的第一個參數為服務器的socket描述字,是服務器開始調用socket()函數生成的,稱為監聽socket描述字;而accept函數返回的是已連接的socket描述字。一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。

  6.send,recv

  ssize_t send(int sockfd, const void *buf, size_t len, int flags);

  ssize_t recv(int sockfd, void *buf, size_t len, int flags);

  send的返回值標識已發送數據的長度,這個值可能比參數len小,這也意味着數據緩沖區沒有全部發出去,要進行后續處理。返回SOCKET_ERROR標識send出錯。

  recv的返回值標識已接收數據的長度。如果連接已關閉,返回值將是0。返回SOCKET_ERROR標識recv出錯。

  9.closesocket

  closesocket( __in SOCKET );//關閉socket

  

  10 :getsockname  獲取本地IP和PORT

  Description:The getsockname function retrieves the local name for a socket.

1 int getsockname(
2   __in          SOCKET s,
3   __out         struct sockaddr* name,
4   __in_out      int* namelen
5 );

  Parameters

  s

Descriptor identifying a socket.

  name

Pointer to a SOCKADDR structure that receives the address (name) of the socket.

  namelen

Size of the name buffer, in bytes.

  Return Value

  If no error occurs, getsockname returns zero. Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling WSAGetLastError.

  11 :getpeername  獲取對端IP和PORT

1 int getpeername(
2   __in          SOCKET s,
3   __out         struct sockaddr* name,
4   __in_out      int* namelen
5 );

 

  Getpeername和getsockname參數一樣。

  12.實現socket的非阻塞(在下一篇有他的用法)

  nt ioctlsocket (SOCKET s,         long cmd,         u_long FAR* argp  );

unsigned long flag=1; 
if (ioctlsocket(sock,FIONBIO,&flag)!=0) 
{ 
    closesocket(sock); 
    return false; 
}

以下是對ioctlsocket函數的相關解釋: 
int PASCAL FAR ioctlsocket( SOCKET s, long cmd, u_long FAR* argp); 
s:一個標識套接口的描述字。 
cmd:對套接口s的操作命令。 
argp:指向cmd命令所帶參數的指針。

  具體的操作可以看https://blog.csdn.net/susubuhui/article/details/7568431

   


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM