Loading

【C++】socket编程(二):最简单的socket通信(上)

-----初更----2018年1月26日(转载请注明出处)----

前言:

昨天把socket基本的知识点总结了一下,今天就开始写第一个socket程序吧,这篇文章中我们要做到的就是本机和本机通信,完成一次发送接收,虽然看着比较简单,但是理解起来也需要费一番功夫,尤其是每个函数的使用。gogogo

#0x01    加载socket库

从现在开始,程序要分为windows版和linux版,linux版就是加载一堆头文件,windows呢就是加载dll后声明一些函数,我们一点点开始,先看windows,想直接看linux的可以跳过windows

windows:

 1 #include <stdio.h>
 2 #include <winsock2.h>//socket头文件
 3 #pragma comment (lib,"ws2_32.lib")//加载socket
 4 
 5 int main()
 6 {
 7     WSADATA wsaData;//生成句柄
 8     WSAStartup(MAKEWORD(2, 2), &wsaData);//初始化
 9 
10     /*codding*/
11 
12     WSACleanup();//结束调用
13     return 0;
14 }

这就是windows的大体结构,ws2_32.lib用的是编译时加载,具体的加载方式和显示加载还是隐式加载大家可以自行查阅,这里不做深究,下面对函数进行分析:

首先看一下WSADATA结构体:

typedef struct WSAData {
  WORD           wVersion;
  WORD           wHighVersion;
  char           szDescription[WSADESCRIPTION_LEN+1];
  char           szSystemStatus[WSASYS_STATUS_LEN+1];
  unsigned short iMaxSockets;
  unsigned short iMaxUdpDg;
  char FAR       *lpVendorInfo;
} WSADATA, *LPWSADATA;

具体每个变量是做什么的就要大废周章了,大家可以点击这里查看微软官方的解释,大概意思就是存放期望调用方使用的Windows套接字规范的版本

再看一下WSAStartup()函数,原型:

int WSAStartup(
  _In_  WORD      wVersionRequested,
  _Out_ LPWSADATA lpWSAData
);

第二个参数lpWSADATA就是WSADATA结构体,第一个WORD参数,用的MAKEWORD()函数,原型如下:

WORD MAKEWORD(
   BYTE bLow,
   BYTE bHigh
);

这个函数主要就是返回一个WORD参数。

接下来看看WSAClearup()函数:

int WSACleanup(void);

官方解释是:The WSACleanup function terminates use of the Winsock 2 DLL(Ws2_32.dll).意思就是不再使用Ws2_32.dll库了。

到此,socket库的调用就说完了,具体还有很多很多没有讲,读者可以自查资料深入研究,最后一节我会给使用到的链接,目前就会使用就行。

Linux:

linux下比较易于理解,主要就是包含一堆头文件就行,demo如下:

 1 #include <stdio.h>
 2 #include <string.h>
 3 #include <stdlib.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 #include <netinet/in.h>
 8 int main()
 9 {
10 /*codding*/
11 }

前三个是C++基础文件,不做赘述,第四个unisted.h是linux下特有的头文件,主要包含了read,write等函数,后三个主要是socket需要的库文件。

#0x02    socket函数

前面讲了一大推分量很小的东西,现在才开始真正的socket编程,这里要明白一个概念,什么是句柄,这个概念在本案例中可以初步认为就是一个int(显然这是错误的,目前先这么理解,因为在linux中并没有句柄)。首先需要调用socket函数,分为windows和linux版本

windows:

SOCKET sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

Linux:

int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

其中SOCKET指的就是句柄,而在linux中用int代替。

现在已经创建一个套接字,供后面的程序使用,记住一个连接用一个套接字,什么意思,就是假如你的机器A和另一个机器B需要TCP连接,这时需要创建一个套接字,然后你的机器A和机器B需要另一个UDP连接,这时候你就需要再创建一个套接字。

win和linux用法是一样的,我们来看一下他的原型:

SOCKET WSAAPI socket(
  _In_ int af,
  _In_ int type,
  _In_ int protocol
);

下面看一个每个值得取值,这里仅仅列出常见取值,其他取值有兴趣可以参考这个

 af:协议族

  • AF_INET(PF_INET):使用IPv4
  • AF_INET6(PF_INET6):使用IPv6

 type:套接字的类型

  • SOCK_STREAM:面向连接的数据传输方式,也就是TCP连接
  • SOCK_DGRAM:无连接的数据传输方式,也就是UDP连接

 protool:传输协议(要和协议和套接字配套,本例指的是TCP或UDP)

  • IPPROTO_TCP:TCP协议
  • IPPROTO_UDP:UDP协议

好了,现在已经创建了套接字, 接下来就要给套接字绑定ip和端口了,查看bind()函数:

#0x03    bind()函数

值得一提的是,bind函数一般用在服务器上,也就是服务端,稍后会讲解客户端需要用什么函数,先看一下bind使用:

 windows:

bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));//Windows

 Linux:

bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

 以上都是实例,参数可能不是很准确,所以看一下函数原型不就知道该怎么写了嘛 

int bind(
  _In_ SOCKET                s,
  _In_ const struct sockaddr *name,
  _In_ int                   namelen
);

 其中,第一个参数s指的就是一开始创建的套接字,不在赘述,这里主要讲一下后面两个参数:

第二个参数是要传入一个sockaddr型的结构体,而这个结构体长啥样呢,我们来看一下

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};

第一个参数还好说,协议族,和创建socket的第一个参数一样,俺么第二个参数呢,14位的地址和端口号应该怎么填?

这时候,我们就要借助另外一个结构体sockaddr_in(至于为什么要借助另外一个结构体,这里涉及到历史遗留问题,感兴趣可以自行查阅)

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};

这里把每个变量位数已经标好了,可以看到位数和上一个结构体一样,所以这里我们用sockaddr_in结构体定义好后替换成sockaddr结构体,都坐下,正常操作是:

    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr));//各位清零
    sockAddr.sin_family = PF_INET;//协议族
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址
    sockAddr.sin_port = htons(1996);//端口
    bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));//Windows

首先创建一个sockaddr结构体,将各位清零(防止乱码),然后设置协议族,ip地址,端口等,最后将这些信息绑定到最开始创建的句柄上,这样一个套接字就完成了绑定

这里介绍一下inet_addr函数,这个函数是将一个ip字符串转换成unsigned long,同样也是历史因素,想具体了解可以查看这里,不过zaiwindows下vs建议换成inetPton(),不过我就是任性啊,就不换,嘻嘻,在linux下这个函数可不可以替换我没有验证,如果有人验证了可以在这篇文章下面留言

htons函数返回一个网络字节数,看一下官方解释,还有htonf()htonl()htonll()等函数,可以好好研究一下

#0x04    connent()函数

 connent函数和bind函数用法一样,只不过connent是客户端的程序,我给列一下函数原型,用法可以在下篇看到

int connect(
  _In_ SOCKET                s,
  _In_ const struct sockaddr *name,
  _In_ int                   namelen
);

函数原型,参数和bind一样,所以用法也一样,不多说。

#0x05 listen()函数

listen函数需要在connect之前,在bind函数之后的服务器端的函数,看名字就知道是用来监听的函数,用法:

Windows/Linux:

listen(servSock, 20);

 

原型是:

int listen(
  _In_ SOCKET s,
  _In_ int    backlog
);

 

比较容易理解,第一个参数就是bind后的套接字,而第二个参数指的是等待连接队列的最大长度,啥意思?

就是说socket有2个缓冲区,一个是发送缓冲区,一个是接受缓冲区,他们有固定的大小,超过了这个大小,数据不就接收不到了嘛,这对文件传输可以一个麻烦,所以这个等待连接队列指的是可以等待多少个缓冲区大小的数据,这里了解到这里就可以了,具体应用可以查看后面的文件传输内容在深入研究

#0x06    参考连接

【1】http://c.biancheng.net/cpp/html/3032.html

【2】http://blog.csdn.net/y396397735/article/details/50655363

【3】http://blog.csdn.net/tennysonsky/article/details/45621341

【4】http://msdn.microsoft.com

 

posted @ 2018-01-27 10:57  _小孟同学  阅读(1729)  评论(0编辑  收藏  举报