首页
社区
课程
招聘
Visual C++网络编程(黑客)学习笔记
发表于: 2012-4-10 18:24 10597

Visual C++网络编程(黑客)学习笔记

2012-4-10 18:24
10597

最近在学VC下的黑客编程,自己整理了一下笔记,发出来,与同学共享。

所有在WIN32平台上的WinSock编程都要经过下列步骤:
(1)定义变量
(2)获得WinSock版本
(3)加载WinSock库
(4)初始化
(5)创建套接字
(6)设置套接字选项
(7)关闭套接字
(8)卸载WinSock库
(9)释放所有资源

注意:对于一款简单的程序来说最后不释放资源可能看不出有什么影响,但如果是多线程或者创建了多个Socket对象而不释放资源的话,那么就会对系统造成较大影响,形成所谓的内存泄露,尤其是有大量操作数据库的动作!C或者C++不像python会自动回收不用的内存,因为他没有自动回收不用内存的机制,所以请务必释放掉所有资源。

传统的网络通信模式——C/S模式,即客户端和服务器模式。
整个程序架构分为两大部分:服务器端和客户端。

服务器端Socket程序流程:socket()——bind()——listen()——accept()——recv()/send()——closesocket()

客户端Socket程序流程:socket()——connect()——send()/recv()——closesocket()

使用Socket的程序在使用之前必须调用WSAStartup函数,此函数在应用程序中用来初始化Windows Socket DLL,只有此函数调用成功后,应用程序才可以调用Windows Socket DLL中的其他API函数,否则后面的任何函数都将调用失败。

WSAStartup函数的原型:

int WSAStartup(
       WORD wVersionRequested,
       LPWSADATA lpWSAData
);

函数参数解释:
wVersionRequested:应用程序欲使用的Windows Socket版本号。
lpWSAData:指向WSADATA的指针。
函数调用后返回一个 int型的值,通过检查这个值来确定初始化是否成功。该函数执行成功后返回0.在程序中调用该函数的形式如下:
WSAAtartup(MAKEWORD(2,2),(LPWSADATA) &WSAData)
其中MAKEWORD(2,2)表示程序使用的是WinSocket 2版本,WSAData用来存储系统传回的关于WinSocket的结构。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 6
支持
分享
最新回复 (16)
雪    币: 1585
活跃值: (190)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
2
初始化WinSock的动态链接库之后,需要在服务器端建立一个用来监听的Socket句柄,可以调用Socket()函数用来建立这个监听的Socket句柄,并定义此Socket所使用的通信协议。
Socket的原型:
SOCKET socket(
       int  af,
       int  type,
       int  protocol
);
函数参数解释:
af:指定应用程序使用的通信协议的协议族。对于TCP/IP族,该参数设为FF_INET.
type:指定要创建的套接字类型。
protocol:指定应用程序所使用的通信协议。

在Winsock 2中,type支持以下3种类型。
(1)SOCK_STREAM:流式套接字。
(2)SOCK_DGRAM:数据报套接字。
(3)SCOK_RAW:原始套接字。

在Winsock 2中,protocol支持以下3种类型。
(1)IPPROTO_UDP:UDP协议,用于无连接数据报套接字。
(2)IPPROTO_TCP:TCP协议,用于流套接字。
(3)IPPROTO_ICMP:ICMP协议,用于原始套接字。

该函数调用成功则返回Socket对象,失败则返回INVALID_SOCKET(调用WSAGetLastError()可知函数调用失败的原因,所有WinSocket的函数都可以使用这个函数来获取调用失败的原因。)

在Windows程序中,并不是用内存的物理地址来标识内存块,文件,任务或者动态装入的模块;相反Windows API给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作。

提示:句柄是 Windows用来标识被应用程序所建立或使用的对象的唯一整数。Windows使用各种各样的句柄标志,如应用程序实例,窗口,控件,位图,GDI对象等。一个Windows应用程序可以用不同的方法获得一个特定项的句柄。通常Windows通过应用程序的引出函数将一个句柄作为参数传给应用程序,应用程序一旦获得了一个确定的句柄,便可以在Windows环境下的任何地方对这个句柄进行操作。
2012-4-10 18:24
0
雪    币: 1585
活跃值: (190)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
3
创建了Socket后,套接字数据结构中有一个默认的IP地址和默认的端口号。服务器端程序必须调用bind函数来为其绑定一个IP地址和一个特定的端口号,这样客户端才知道要连接服务器端的哪个IP地址的哪个端口号。客户端程序一般不必调用bind函数来为其Socket绑定IP地址和端口号。该函数调用成功返回0,否则返回SOCKET_ERROR.
bind函数原型:
int bind(
         SOCKET s,
         const struct sockaddr FAR *name,
          int namelen
);

函数参数解释:
s:指定待绑定的Socket描述符。
name:指定一个sockaddr结构。
namelen:是name结构体的大小。

name是一个sockaddr结构类型,sockaddr结构类型定义如下。参数sa_family指定地址族,对于TCP/IP族的套接字,将其设置为AF_INET.
struct sockaddr(
       u_short sa_family;
       char sa_data[14];
);
对于TCP/IP族的套接字进行绑定时,通常使用另一个地址结构:
struct sockaddr_in(
        short sin_family;
        u_short sin_port;
        struct in_addr sin_addr;
        char sin_zero[8];
);
函数参数解释:
sin_family:设置TCP/IP协议族类型AF_INET.
sin_port:指定端口号。
sin_addr:结构体中只有一个唯一的字段s_addr,表示IP地址,该字段是一个整数,一般用函数inet_addr()把字符串形式的IP转换成unsigned long型的整数值后再发送给s_addr.

提示:有的服务器是多宿主机,一般有两个网卡。运行在这样的服务器上的服务器端程序在为其Socket绑定IP地址时可以把htonl(INADDR_ANY)置给s_addr。好处是不管那个网段上的客户端程序都能与该服务端程序通信。如果只给运行在多宿主机上的服务器端程序的Socket绑定一个固定的IP地址,那么只有与该IP地址处于同一个网段上的客户端程序才能与该服务器端程序通信。使用0来填充sin_zero数组,目的是让sockaddr_in结构的大小与sockaddr结构的大小保持一致。

下面是一个bind函数调用的例子.
//......
           struct sockaddr_in name;
           name.sin_family = AF_INET;
           name.sin_port = htons(80);
           name.sin_addr.s_addr = htonl(INADDR_ANY);
           int namelen = sizeof(name);
           bind(sSocket, (struct sockaddr *)&name, namelen);
//......

以上代码中,如果不需要特别指明IP地址和端口号的值,那么可以设定IP地址为INADDR_ANY,Port为0。Windows Sockets会自动为其设定适当的IP地址和Port(1024——5000)。如果想得到该IP地址和端口,可以调用getsockname()函数来获得其被设定的值。
2012-4-10 18:25
0
雪    币: 243
活跃值: (247)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
哈哈,对网络不懂。看一下
2012-4-10 18:30
0
雪    币: 1585
活跃值: (190)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
5
服务器端的Socket对象绑定完成之后,服务器端必须建立一个监听队列来接受客户端发送的请求。listen()函数使服务器短的Socket进入监听状态,并设定可以建立的最大连接数(一次同时连接不能超过5个IP)。
listen()函数原型:
int listen(
          SOCKET s,
          int backlog
);
函数参数解释:
s:指定监听的Socket描述符。
backlog:一次同时连接的最大数目。
该函数调用成功返回0,否则返回SOCKET_ERROR.服务器端的Socket调用完listen()后使其套接字s处于监听状态。处于监听状态的流套接字s将维护一个客户连接请求队列,最多容纳backlog个客户连接请求。

当客户端发出连接请求时,服务器端hwnd视窗会收到Winsock Stack送来自定义的一个消息,为了使服务器端接受客户端的连接请求,就要使用accpet()函数。处于监听状态的流套接字s从客户端连接请求队列中取出排在最前面的一个客户请求,并且创建一个新的套接字来与客户端套接字共同创建连接通道。原先处于监听状态的套接字继续处于监听状态,等待客户端的连接,这样服务器端和客户端才算正式完成通信程序的连接动作。如果创建连接通道成功就返回新创建套接字的描述符,以后与客户端套接字交换数据的是新创建的套接字;如果失败则返回INVALID_SOCKET.
accpet函数原型:
SOCKET accept(
       SOCKET s,
       struct sockaddr FAR *addr,
        int FAR *addrlen
);
函数参数解释:
s:处于监听状态的流套接字。
adder:用来返回新创建的套接字的地址结构。
addrlen:指明用来返回新创建的套接字的地址结构的长度。

下面是accept()函数的示例。
//......
           struct sockaddr_in addr;
            int addrlen;
            addrlen  = sizeof(addr);
            addr = accept(sSocket, (struct sockaddr *)&addr, &addrlen);
//......
2012-4-10 18:51
0
雪    币: 136
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
怎么都是socket啊。。
2012-4-10 19:14
0
雪    币: 1737
活跃值: (110)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
没接触过哦,看看先
2012-4-10 19:25
0
雪    币: 183
活跃值: (55)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
8
lz用心人啊..顶一个
2012-4-10 20:47
0
雪    币: 239
活跃值: (133)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
9
IOCP么来弄弄
2012-4-10 21:48
0
雪    币: 71
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
lz用心人啊
2012-4-10 21:52
0
雪    币: 1585
活跃值: (190)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
11
当客户端向服务器端发出连接请求,服务器端即可用accept函数实现与客户

端的连接。为了达到服务器端的Socket在恰当的时候与从客户端发出来的连

接请求建立连接,服务器端需要使用WSAAsyncSelect函数让系统主动来通

知服务器端程序有客户端提出连接请求了。函数调用成功则返回0,失败则

返回SOCKET_ERROR。
WSAAsyncSelect函数原型:
int WSAAsyncSelect(
         SOCKET s,
         HWND hWnd,
         unsigned int wMsg,
         long lEvent
);
函数参数解释:
s:Socket对象。
hWnd:接收消息的窗口句柄。
wMsg:传给窗口的消息。
lEvent:被注册的网络事件,即是应用程序向窗口发送消息的网络事件。
lEvent值为下列值:

FD_READ,FD_WRITE,FD_OOB,FD_ACCEPT,FD_CONNECT,FD_CLOSE的组合

。各个值的具体含义如下:
                             FD_READ:希望在套接字S收到数据时收到消息。
                             FD_WRITE:希望在套接字S发送数据时收到消息。
                     FD_ACCEPT:希望在套接字S收到连接请求时收到消息。
                             FD_CONNECT:希望在套接字S连接成功时收到消息。
                             FD_CLOSE:希望在套接字S连接关闭时收到消息。
                             FD_OOB:希望在套接字S收到带外数据时收到消息。
该函数在具体应用时,wMsg应是在应用程序中定义的消息名称,而消息结

构中的lParam则为以上各种网络事件的名称。所以可在窗口处理自定义消

息函数中使用以下结构代码来响应Socket的不同事件,代码如下:
switch(lParam)
{
//响应FD_READ的函数方法实现
case FD_READ:
            ...
         break;
//响应FD_WRITE的函数实现方法
case FD_WRITE:
            ...
          break;
//响应FD_ACCEPT的函数实现方法
case FD_ACCEPT:
            ...
          break;
//...
}

结束服务器端与客户端的通信连接,可由服务器端或者客户端的任一端发出

请求,只要调用closesocket()就可以了。同样,要关闭服务器端监听状态的

Socket也是利用该函数。在调用closesocket()函数关闭Socket之前,与程序

启动时调用WSAStartup()函数相对应。程序结束前需要调用WSACleanup()

来通知Winsock Stack释放Socket所占用的资源。该函数调用成功返回0 ,

否则返回SOCKET_ERROR.
WSAStartup()函数原型:
int WSAStartup();
该函数在应用程序完成对请求的Socket库的使用后调用,来解除与Socket库

的绑定并且释放Socket库所占用的资源。该函数一般用在网络程序结束的地

方调用。
closesocket()原型:
int closesocket(
           SOCKET s
);
函数参数解释:
s:表示要关闭的套接字。
函数成功执行返回0,否则返回SOCKET_ERROR.
每个进程中都有一个套接字描述符表,表中的每个套接字描述符都对应了一

个位于操作系统缓冲区中的套接字数据结构。因此可能有几个套接字描述符

指向同一个套接字数据结构。套接字数据结构中专门有一个字段存放该结构

被引用的次数,即有多少个套接字描述符指向该结构。当调用closesocket

函数时,操作系统先检查套接字数据结构中的该字段的值。如果值为1就表

示只有一个套接字描述符指向它,因此操作系统就先s在套接字描述符表中

对应的那条表项清除,并且释放s对应的套接字数据结构;如果该字段值大

于1,那么操作系统仅仅清除s在套接字描述符表中的对应表项,并且把s对

应的套接字数据结构的引用次数减1.
2012-4-10 22:37
0
雪    币: 99
活跃值: (40)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
挺复杂的
2012-4-10 22:52
0
雪    币: 1585
活跃值: (190)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
13
上几节主要介绍了Windows Socket编程中服务器端操作Socket的编程,下面几节主要介绍客户端Socket的操作。客户端Socket编程主要涉及建立Socket连接和发送连接请求等。
客户端应用程序首先也是调用WSAStartup()函数的初始化Winsock的动态链接库,然后同样调用socket()来建立一个TCP或UDP Socket (相同协议的Sockets才能相通,TCP对TCP,

UDP对UDP)。与服务器端Socket不同的是,客户端的Socket可以调用bind()函数来指定IP或者PORT,但是也可以不调用而由Winsock来自动设定IP和PORT。

当客户端程序要连接服务器时,客户端程序的Socket调用connect()函数监听,与name所指定的特定端口上的服务器端Socket进行连接。调用成功返回0,失败返回

SOCKET_ERROR.

connect()函数的原型:
int connect(
        SOCKET s,
        const struct sockaddr FAR *name,
        int namelen
);
函数参数解释:
s:客户端流套接字。
name:要连接的套接字的地址结构。
namelen:指明套接字的地址结构的长度。

下面是connect函数示例:
//...
        struct sockaddr_in name;
        memset((void*)&name, 0, sizeof(name));
        name.sin_family = FA_INET;
        name.sin_port = htons(80);
        name.sin_addr.s_addr = inet_addr("");
        connect(sSocket, (struct sockaddr *)&name, sizeof(name));
//...

服务器端和客户端分别创建Socket并连接后,就开始数据的传送。网络编程数据传送涉及两大协议:TCP/IP和UDP。下节将介绍TCP Socket与UDP Socket在传送数据时的特性以及

Socket数据报的发送方式。
2012-4-11 01:12
0
雪    币: 1015
活跃值: (235)
能力值: ( LV12,RANK:440 )
在线值:
发帖
回帖
粉丝
14
谢谢楼主分享啊,建议最后用一个word文档汇总
2012-4-11 21:27
0
雪    币: 1585
活跃值: (190)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
15
等啥时弄完了,打包一个,传上来!
2012-4-11 21:38
0
雪    币: 231
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
谢谢楼主分享,建议最后用一个word文档汇总~支持下
2012-4-11 21:56
0
雪    币: 1585
活跃值: (190)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
17
下面介绍TCP Socket与UDP Socket
Stream(TCP) Socket:提供双向,可靠,有次序,不重复的数据传送。
Datagram(UDP) Socket:虽然提供双向的通信,但没有可靠,有次序,不重复的保证。
一般情况下TCP Socket的数据发送和接受是调用send()和recv()这两个函数来完成的;UDP Socket则调用sendto()和recvfrom()这两个函数。这两对函数调用成功返回发送或接受的数据的长度,失败返回SOCKET_ERROR。
函数原型:
int send(
           SOCKET s,
           const char FAR *buf,
           int len,
           int flags
);
参数解释:
s:指定发送端套接字描述符。
buf:指明一个存放应用程序要发送数据的缓冲区。
len:指明实际要发送的数据的字节数。
flag:一般置0。

不论是客户端还是服务端应用程序都用send函数向TCP连接的另一端发送数据。客户端一般用send向服务器发送请求,服务端一般用send向客户端发送应答。

对于同步Socket的send函数的执行流程,当调用该函数时,send先比较待发送数据的长度(len)和套接字s的发送端缓冲区长度。
如果len>s,函数返回SOCKET_ERROR,发送失败;
如果len<s或者len=s,那么send先检查协议是否正在发送s的发送缓冲区的数据,如果是,那么就等协议把数据发送完,如果协议还没有开始发送s的发送缓冲区中的数据或者s的发送缓冲区中没有数据,那么send就比较s的发送缓冲区的剩余空间和len的大小。如果len>剩余空间,send就一直等待协议把s的的发送缓冲区中的数据发送完;如果len<s的发送缓冲区的剩余空间,send就仅仅把buf中的数据复制到剩余空间里(注意,并不是send把s的发送缓冲区中的数据传到连接的另一端,而是协议传送的,send仅仅是把buf中的数据复制到s的发送缓冲区的剩余空间)。

如果send函数复制数据成功就返回实际复制的字节数;如果send在复制数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
send函数把buf中的数据成功复制到s的发送缓冲区的剩余空间后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR.

函数原型:
int recv(
        SOCKET s,
        char FAR *buf,
        int len,
        int flags
);
参数解释:
s:指定接收端套接字描述符。
buf:指明一个缓冲区,用来存放recv函数收到的数据。
len:指明buf的长度。
flags:一般置0.
不论是客户端还是服务器端应用程序都用recv函数从TCP/IP连接的另一端接受数据。
对于同步Socket的recv函数的执行流程,当应用程序调用recv函数时,recv函数先等待s的发送缓冲区中的数据传输完毕。如果协议在传送s的发送缓冲区中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;如果s的发送缓冲区中没有数据或者数据被协议成功发送完毕,那么recv先检查套接字s的接收缓冲区,如果s的接收缓冲区中没有数据或者协议正在发送数据,那么recv就一直等待,直到协议把数据发送完毕。
当协议把数据接收完毕,recv函数就把s的接收缓冲区中的数据拷贝到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下需要调用几次recv函数才能把s的接收缓冲区中的数据拷贝完。recv函数仅仅是拷贝数据,真正的接收数据是协议完成的)。
recv函数返回其实际拷贝的字节数,如果recv在拷贝时出错,那么返回SOCKET_ERROR,如果recv在等待协议接收数据时网络中断,那么它返回0.

函数原型:
int send(
         SOCKET s,
         const char *buf,
         int len,
         int flags,
         const struct sockaddr* to,
         int tolen
);
参数解释:
s:指定发送端套接字描述符。
buf:指明一个存放应用程序要发送数据的缓冲区。
len:指明实际要发送的数据的字节数。
flags:一般置0.
to:指向目的地址的指针。
tolen:to的长度。

提示:因为在无连接的情况下,本地socket并没有和远程机器建立连接,所以在发送数据时应指明目的地址,因此sendto比send函数多了两个参数:to表示目的机器的IP和PORT,而tolen一般赋为sizeof(struct sockaddr)。

对于Datagram Socket而言,若是datagram的大小超过限制,则不会传送任何数据,并会传回错误值。对于Stream Socket而言,在Blocking模式下,若是传送系统内的存储空间不够存放这些要传送的数据,send()将会被block住,知道数据传完为止;如果该Socket被设定为Non-Blocking模式,那么将视目前的output buf空间有多少就送出多少数据,并不会被block住.flags的值可设为0或MSG_DONTROUTE及MSG_OOB的组合。

函数原型:
int recvfrom(
        SOCKET s,
        char *buf,
        int     len,
        int     flags,
        struct  sockaddr *from,
        int *fromlen
);
参数解释:
s:指定接收端套接字描述符。
buf:指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据。
len:指明buf的长度。
flags:一般置0.
from:数据包的来源地址。
fromlen:from的长度。

提示:因为在无连接的情况下,本地socket并没有和远程机器建立连接,所以收到UDP数据时,要通过from参数判断来源。
2012-4-12 00:59
0
游客
登录 | 注册 方可回帖
返回
//