winsocket之TCP/UDP编程

简介:

一.概述:

本次练习的是TCP/UDP套接字编程,使用的是winsocket,对主要的库函数进行简绍,并实现了一个程序:实现服务器与客户端之间的通信,在服务器端实现记录用户名和密码,客服端可以实现用户名和密码的输入和查找,并且检查是否匹配。(参考  <<Visual C++网络编程>>)

PS: 127.0.0.1是回路地址,用于在同一台机器上测试代码。端口号要大于1024。



二.基于TCP/UDP协议的套接字编程详解:

基本 TCP 套接字编程讲解

基于 TCP 的套接字编程的所有客户端和服务器端都是从调用socket 开始,它返回一个套接字描述符。客户端随后调用connect 函数,服务器端则调用 bind、listen 和accept 函数。套接字通常使用标准的close 函数关闭,但是也可以使用 shutdown 函数关闭套接字。下面针对套接字编程实现过程中所调用的函数进程分析。以下是基于 TCP 套接字编程的流程图:



典型的UDP客户/服务器程序函数调用图:




三.相关数据结构:

(1).sockaddr:sockaddr用来保存一个套接字

struct sockaddr
{
    unsigned short int sa_family; //指定通信地址类型,如果是TCP/IP通信,则值为AF_inet
    char sa_data[14]; //最多用14个字符长度,用来保存IP地址和端口信息};
 }

(2).

sockaddr_in的功能与socdaddr相同,也是用来保存一个套接字的信息,不同的是将IP地址与端口分开为不同的成员,定义如下:

struct sockaddr_in
{
    unsigned short int sin_family; //指定通信地址类型
    uint16_t sin_port; //套接字使用的端口号
    struct in_addr sin_addr; //需要访问的IP地址
    unsigned char sin_zero[8]; //未使用的字段,填充为0};
 }

在这一结构中,in_addr也是一个结构体,定义如下,用于保存一个IP地址:

struct in_addr
{
    uint32_t  s_addr;
};

(3).WSAData:包含Winsock库的版本信息,这个结构是在调用函数WSAStartup时由系统填入。

struct WSAData { 
WORD wVersion; 
WORD wHighVersion; 
char szDescription[WSADESCRIPTION_LEN+1]; 
char szSystemStatus[WSASYSSTATUS_LEN+1]; 
unsigned short iMaxSockets; 
unsigned short iMaxUdpDg; 
char FAR * lpVendorInfo; 
}; 
    wVersion为你将使用的Winsock版本号,

    wHighVersion为载入的Winsock动态库支持的最高版本,注意,它们的高字节代表次版本,低字节代表主版本。
    szDescription与szSystemStatus由特定版本的Winsock设置,实际上没有太大用处。
    iMaxSockets表示最大数量的并发Sockets,其值依赖于可使用的硬件资源。
    iMaxUdpDg表示数据报的最大长度;然而,获取数据报的最大长度,你需要使用WSAEnumProtocols对协议进行查询。

(4).SOCKET:即套接字句柄,为一个32位的整数。

typedef unsigned int SOCKET




四.Winsock相关函数:

(1).WSAStartup函数:初始化Winsock

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData)

 wVersionRequested参数:是一个WORD(双字节)数值,它指定了应用程序需要使用的Winsock版本.
    主版本号在 低字节, 次版本号在 高字节。

    实例:希望版本号为 1.2 可以如下代码:
    wVersionRequested = 0x0201。

    lpWSAData参数:指向WSADATA数据结构的指针,该结构用于返回本机的Winsock系统实现的信息.
    该结构WhighVersion和wVersion两个域系统支持的最高版本,后者是系统希望调用者使用的版本.

 函数成功 返回0; 否则返回错误码.

一般可以这样初始化:


WSADATA wsaData;

WSAStartup(0x0202, &wsaData); 


(2).WSACleanup函数:Winsock程序在退出之前都必须要调用WSAClernup,以便系统可以释放资源。

int WSACleanup(void)


(3).WSAGetLastError函数:当一个Winsock函数返回一个失败值时,调用这个函数可以获取具体的失败原因。

int WSAGetLastError(void)


(4).inet_addr函数:地址转换函数

ussigned long inet_addr(const char FAR* cp)

例如:inet_addr("127.0.0.1")


(5).字节序转换函数:

u_long htonl(u_long hostlong);

u_short htons(u_short hostshort);

u_long ntonl(u_long netlong);

u_short ntons(u_short netshort);

htonl和htons用于把主机号字节序转换为网络字节序,ntonl和ntons则相反。(host:主机)



五.与实现通信相关的函数:

(1).sock函数:返回一个 套接字句柄。

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


  1.  * 说明:  

  2.  * socket类似与open对普通文件操作一样,都是返回描述符,后续的操作都是基于该描述符;  

  3.  * family 表示套接字的通信域,不同的取值决定了socket的地址类型,其一般取值如下:  

  4.  * (1)AF_INET         IPv4因特网域  

  5.  * (2)AF_INET6        IPv6因特网域  

  6.  * (3)AF_UNIX         Unix域  

  7.  * (4)AF_ROUTE        路由套接字  

  8.  * (5)AF_KEY          密钥套接字  

  9.  * (6)AF_UNSPEC       未指定  

  10.  *  

  11.  * type确定socket的类型,常用类型如下:  

  12.  * (1)SOCK_STREAM     有序、可靠、双向的面向连接字节流套接字  

  13.  * (2)SOCK_DGRAM      长度固定的、无连接的不可靠数据报套接字  

  14.  * (3)SOCK_RAW        原始套接字  

  15.  * (4)SOCK_SEQPACKET  长度固定、有序、可靠的面向连接的有序分组套接字  

  16.  *  

  17.  * protocol指定协议,常用取值如下:  

  18.  * (1)0               选择type类型对应的默认协议  

  19.  * (2)IPPROTO_TCP     TCP传输协议  

  20.  * (3)IPPROTO_UDP     UDP传输协议  

  21.  * (4)IPPROTO_SCTP    SCTP传输协议  

  22.  * (5)IPPROTO_TIPC    TIPC传输协议  


(2).closesocket函数:关闭一个套接字。

int closesocket(SOCKET s);

传回值: 成功 返回0 , 失败 返回 SOCKET_ERROR 。


(3).shutdown函数:停止 Socket 接收/传送的功能

int shutdown(SOCKET s,int how)

参数: s :Socket 的识别码,

          how :代表该停止那些动作的标帜 

传回值: 成功返回 0 ,失败 返回 SOCKET_ERROR 。
若 how 的值为 0,则不再接收资料。 
若 how 的值为 1,则不再允许传送资料。 
若 how 的值为 2,则不再接收且不再传送资料。 
注意:shutdown() 函式并没有将 Socket 关闭,所以该 Socket 所占用之资源必须在呼叫closesocket() 之后才会释放。 


(4).bind函数:把相关套接字句柄绑定到addr地址,绑定之后客户端/服务器就可以通过该地址连接到服务器/客户端。

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

   s参数: 为一个套接字句柄;  

   addr参数:是一个指向特定协议地址结构的指针;  

   addrlen参数:是地址结构的长度;  

传回值: 成功 返回0 , 失败 返回 SOCKET_ERROR 。

PS:当addr结构体中的sin_addr.s_addr为INADDR_ANY时,表示绑定到通配地址(即没有指定的mac地址),这时可以接受来自所有网络接口的连接请求,适合于有多个网卡的情况。


(5).listen函数:设定 Socket 为监听状态,准备被连接。

int listen(SOCKET s,int backlog)

参 数: s Socket 的识别码,

              backlog 未真正完成连接前彼端的连接要求的最大个数 (连接队列中的最大数)
传回值: 成功 返回0 , 失败 返回 SOCKET_ERROR 。


(6).accept函数:从已完成连接队列队头返回下一个已完成连接;若已完成连接队列为空,则进程进入睡眠。

   int accept(SOCKER s, struct sockaddr FAR* addr, int FAR* addrlen)     

参数 s:已经绑定并进入监听状态的套接字句柄。

   addr:用于保存客户端的地址信息。

   addrlen:用于保存客户端的地址信息的结构体大小

PS:若没有连接请求等待处理,accept会阻塞直到一个请求到来;  

返回值:若成功返回套接字描述符,出错返回INVALID_SOCKET;如果成功,此时的套接字为已连接套接字,后面的通信是用这个已连接套接字来描述的。


(7).recv函数:用于TCP流式套接字编程中接受来自客户端的消息。

int recv(SOCKET s, char FAR* buf, int len, int flags)

参数:s:accept返回的一个已连接套接字

    buf:用于接受数据的缓存区

    len:buf的长度,以字节问单位。

    flags:用来影响recv的行为,一般为0;

返回值:成功时,返回实际接收的字节数,失败返回SOCKET_ERROR。

PS:recv为阻塞式的等待有数据可接收。


(8).send函数:用于TCP流式套接字编程中发送消息到客户端。

int send(SOCKET s, char FAR* buf, int len, int flags)

参数:s:accept返回的一个已连接套接字

    buf:用于发送数据的缓存区

    len:buf的长度,以字节问单位。

    flags:用来影响recv的行为,一般为0;

返回值:成功时,返回实际发送的字节数,失败返回SOCKET_ERROR。


(9).connect函数:连接服务器地址,使s成为一个已连接套接字。

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

   s参数: 为一个套接字句柄;  

   addr参数:是一个指向特定协议地址结构的指针;  

   addrlen参数:是地址结构的长度;  

传回值: 成功 返回0 ,此时代表已经和服务器连接成功,s成为一个已连接套接字; 失败 返回 SOCKET_ERROR 。


(10).recvfrom函数用于UDP报文套接字编程中接受来自地址为from的主机消息。

int recvfrom(SOCKET s, char FAR* buf, int len, int flags,

         struct socket FAR* from, int FAR* fromlen)

参数:s为一个已连接套接字。

    buf:数据接收缓冲区。

    len:数据接收缓存区的大小。

    flags:用于控制recvfrom行为的一些标志,一般为0.

    from:对方的套接字地址。

    fromlen:对方的套接字地址结构的大小。

返回值:成功返回实际接受的字节数,失败返回SOCKET_ERROR;如果buf大小不足以容纳接收到的数据报,那么返回一个WSAEMSGSIZE错误。

PS:UDP套接字编程中的接收和发送信息都是以数据报为单位的。每次recvfrom都是接收一个独立的数据报,不同的recvfrom所接收的数据报之间没有任何关系(即不会出现一个recvfrom接收一个数据报的一部分,另一个接收这个数据报的另一部分的情况)。


(11)..sendto函数用于UDP报文套接字编程中发送到地址为from的主机消息。

int sendto(SOCKET s, char FAR* buf, int len, int flags,

         struct socket FAR* to, int FAR* tolen)

参数:s为一个已连接套接字。

    buf:发送数据的缓冲区。

    len:发送数据的缓存区大小。

    flags:用于控制sendto行为的一些标志,一般为0.

    to:对方的套接字地址。

    tolen:对方的套接字地址结构的大小。

返回值:成功返回实际发送的字节数,失败返回SOCKET_ERROR;要发送的字节数为len时,如果len大于UDP数据报的最大值,那么返回一个WSAEMSGSIZE错误。

PS:对于UDP而言,sendto的行为是全部或者没有(all or nothing),要么成功发送所有要求发送的字节,要么发送失败。




六.实际步骤参考上图。




七.实现功能的代码:

(1)TCP套接字编程:

服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include<iostream>
#include<string.h>
#include<winsock2.h>
#pragma comment(lib,"Ws2_32.lib")
using  namespace  std;
 
struct  Data
{
     char * _usser;
     char * _possword;
 
     Data()
         :_usser( new  char [1024])
         , _possword( new  char [1024])
     {}
};
 
 
const  int  PORT = 2000;
const  int  LEN = 1024;
char  buf[LEN];
Data messeage[LEN];
 
SOCKET sListen;
sockaddr_in saListen; 
sockaddr_in saClient;
SOCKET serverToClient;
 
void  Find()    //查找用户名 和 密码是否匹配存在
{
     char * msUsser =  "请输入你要查找的用户名:" ;
     send(serverToClient, msUsser,  strlen (msUsser), 0);
 
     memset (buf, 0, LEN);
     int  ret = recv(serverToClient, buf, LEN, 0); //接收用户名
     for  ( size_t  i = 0; i < LEN; i++)
     {
         if  ( strcmp (messeage[i]._usser,buf) == 0)
         {
             char * msPsw =  "请输入所查找用户名的密码:" ;
             send(serverToClient, msPsw,  strlen (msPsw), 0);
 
             memset (buf, 0, LEN);
             int  ret = recv(serverToClient, buf, LEN, 0); //接收密码
 
             if  ( strcmp (messeage[i]._possword, buf) == 0)       //s注意用trcmp函数
             {
                 char * ms =  "用户名和密码匹配存在\n请选择->" ;
                 send(serverToClient, ms,  strlen (ms), 0);
             }
             else
             {
                 char * mssage =  "用户名正确 密码错误\n请选择->" ;
                 send(serverToClient, mssage,  strlen (mssage), 0);
             }
 
             memset (buf, 0, LEN);
             recv(serverToClient, buf, LEN, 0);
             return ;
         }
     }
 
     char * msUsserNot =  "用户名不存在\n请选择->" ;
     send(serverToClient, msUsserNot,  strlen (msUsserNot), 0);
 
     memset (buf, 0, LEN);
     recv(serverToClient, buf, LEN, 0);
}
 
void  Record()
{
     size_t  index = 0;
     while  ( true )
     {  
         while (buf[0] ==  '2' )     //只有用户名和密码都输入完才能查找//      while
         {
             Find();
         }
 
         if  (buf[0] ==  '0' )    //如果客服端输入0   则退出连接
         {
             char * ms =  "退出成功" ;
             int  len =  strlen (ms);
             send(serverToClient, ms, len, 0);
 
             cout <<  "服务器退出连接!!!"  << endl;
             return ;
         }
 
 
         char * msUsser =  "请输入用户名:" ;
         int  len =  strlen (msUsser);
         send(serverToClient, msUsser, len, 0);
 
         memset (buf, 0, LEN);
         int  ret = recv(serverToClient, buf, LEN, 0); //接收用户名
 
         if  (ret == SOCKET_ERROR)   //表示接收失败
         {
             cout <<  "Client closed !!!"  << endl;
             int  size =  sizeof (sockaddr);
             serverToClient = accept(sListen, (sockaddr*)&saClient, &size);   //重新创建一个连接套接字 
             cout <<  "accept new"  << endl;
         }
 
         strcpy_s(messeage[index]._usser,1024, buf);    //注意 用户名不用++
 
         char * msPsw =  "请输入密码:" ;
         send(serverToClient, msPsw,  strlen (msPsw), 0);
 
         memset (buf, 0, LEN);
         int  retTwo = recv(serverToClient, buf, LEN, 0); //接收密码
 
         if  (retTwo == SOCKET_ERROR)   //表示接收失败
         {
             cout <<  "Client closed !!!"  << endl;
             int  size =  sizeof (sockaddr);
             serverToClient = accept(sListen, (sockaddr*)&saClient, &size);   //重新创建一个连接套接字 
             cout <<  "accept new"  << endl;
         }
 
         strcpy_s(messeage[index++]._possword,1024, buf);
         memset (buf, 0, LEN);
 
         char * choice =  "请选择->" ;
         send(serverToClient, choice,  strlen (choice), 0);
 
         memset (buf, 0, LEN);
         recv(serverToClient, buf, LEN, 0); //接收客户的选择
     }
}
 
 
int  main()
{
     WSADATA wsaData;
     WSAStartup(0x0202, &wsaData);    //初始化winsock
 
     sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);   //创建一个套接字
 
     //绑定分配的 port 和 ip地址
     saListen.sin_family = AF_INET;
     saListen.sin_addr.s_addr = htonl(INADDR_ANY);
     saListen.sin_port = htons(PORT);
     bind(sListen, (sockaddr*)&saListen,  sizeof (sockaddr));
 
     listen(sListen, 5);       //进入监听状态
 
     int  size =  sizeof (sockaddr);
     serverToClient = accept(sListen, (sockaddr*)&saClient, &size);    //创建一个连接套接字
     
     int  ret = recv(serverToClient, buf, LEN, 0);
     if  (ret == SOCKET_ERROR)   //表示接收失败
     {
         cout <<  "Client closed !!!"  << endl;
         int  size =  sizeof (sockaddr);
         serverToClient = accept(sListen, (sockaddr*)&saClient, &size);   //重新创建一个连接套接字 
         cout <<  "accept new"  << endl;
     }
 
     Record();
     
 
     return  0;
}





客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//本次练习存在很多漏洞     
//当相同的代码段重复出现多次时  变量的命名很重要
//客户端只要接受和发送信息就可   关键协议在服务端实现
//注意  send  与 recv  要匹配使用
 
#include<iostream>
#include<winsock2.h>
#pragma comment(lib,"WS2_32.lib")
using  namespace  std;
 
const  int  PORT = 2000;
const  int  LEN = 1024;
 
int  main()
{
     WSADATA wsaData;
     WSAStartup(0x0202, &wsaData);
 
     SOCKET client;
     client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 
     sockaddr_in saServer = {0};
     saServer.sin_family = AF_INET;
     saServer.sin_addr.s_addr = inet_addr( "127.0.0.1" );
     saServer.sin_port = htons(PORT);
     int  ret = connect(client, (sockaddr*)&saServer,  sizeof (saServer));
 
     if  (ret == 0)
     {
         printf ( "connect access :%d:%d\n" , saServer.sin_addr.s_addr, saServer.sin_port);
     }
     else
     {
         printf ( "connect faild :%d:%d\n" , saServer.sin_addr.s_addr, saServer.sin_port);
     }
 
     cout <<  "............0:退出    1:输入    2:查找 ..............."  << endl;
     
     char  buf[LEN];
     printf ( "请选择->  " );
     gets_s(buf);
     send(client, buf,  strlen (buf), 0);
 
     while  ( true )
     {
         
         memset (buf, 0, LEN);
         int  retUsser = recv(client, buf, LEN, 0);
         buf[retUsser] =  '\0' ;
         cout << buf << endl;
 
         gets_s(buf);   
         send(client, buf,  strlen (buf), 0);
 
     }
 
     return  0;
}

执行结果:

wKiom1ci5y2yZ0nYAABIsfdw7bg009.png

wKioL1ci5_vSj8PhAAAf_xDGab0633.png

wKioL1ci5_zyNLQaAAAhPt5tZrM501.png

wKiom1ciuyjwW4TZAAAWBbbAZLs871.png



(2).UDP套接字编程:

服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40