技术宅的结界

 找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 7418|回复: 39
收起左侧

【TCPIP】Winsock在Windows下的编程教程(C语言)

[复制链接]

1028

主题

2283

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
201
威望
271 点
宅币
17523 个
贡献
35220 次
宅之契约
0 份
在线时间
1676 小时
注册时间
2014-1-26
发表于 2014-3-16 22:57:19 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有帐号?立即注册→加入我们

x
Winsock是什么?
Winsock是用来借助网络,让多台设备互相发送数据的一个库。我们用浏览器上网的原理,就是浏览器通过使用Winsock来发送请求给服务器,然后服务器通过Winsock把网页的内容发给浏览器。
或者举个例子,我们使用QQ聊天的时候,是什么东西把我们要传达给对方的话发送到对方电脑上的呢?是Winsock。
这么说大家应该懂什么是Winsock了吧?就是不同的电脑之间互相发送信息的一个工具。Winsock具有可移植性,能在不同的环境下使用(除了Windows系统以外,安卓手机、PSP游戏机、苹果电脑、各种PAD等都支持Winsock)
Winsock是C语言的库,C艹也能用。当然别的语言也有使用Winsock的方法,但是我这里就不赘述了。我主要介绍C语言如何使用Winsock进行数据传输。
对于我的教程有什么不懂的请查询MSDN。

1、先了解一下网络是什么东西。
NET.GIF
图中那个大圈上的用户都是用猫连的网,因此他们的IP地址就是这个大圈内的公网IP。
路由器的作用是让多个用户只通过一个上网账号就能上网。每个路由器都连着一个圈,这个圈就是这些共用一个上网账号的复数个用户。
通过设置路由器的转发机制,可以把所有发往路由器的数据包全部发到局域网的其中一台电脑。这样可以直接实现直连。
许多局域网游戏,都可以通过这个方法来联机。比如KKND2。
所谓桥接其实和路由器的性质差不多。

2、Winsock的大致使用流程:
①初始化Winsock(Windows下调用WSAStartup进行初始化,Linux下貌似不需要)
②创建一个“套接字”。你可以把“套接字”理解为“用来收发数据包的那么一个东西”、“‘套接字’是用来发东西的”
③用完了的“套接字”记得关闭就行。不关闭会妨碍别的程序的通信。使用closesocket把不用的套接字关闭掉吧。
④用完Winsock之后,Windows下记得调用WSACleanup结束你对Winsock的使用。

3、一些概念
①IP地址:用来定位一个电脑的位置。
②域名:所谓域名就是……举个例子,“www.baidu.com”、“www.0xaa55.com”这样的东西就是域名。所谓“域名解析”就是把域名变成IP地址。
③端口:一个电脑(或别的设备)进行网络数据传输的通道。不同通道之间互相不影响。

本教程主要是以VC6编程为标准的,包括VC6建立工程的细节在内。

第一步:建立工程

1、建立一个Windows Console Application(这里不需要用到GUI,因此用CUI来演示详细的效果。)
我这里建立了一个名为Chat的工程,是空工程,这样便于设置。
CHAT.PNG

2、创建一个新的C源码文件。点开文件视图然后点“文件->新建->C++ Source File”文件名写Entry.c。注意必须是Entry.c而不是Entry.cpp
NEW.PNG

3、快速敲入以下代码为后面的教程做好准备:
[C] 纯文本查看 复制代码
#include<stdio.h>
#include<winsock2.h>

int main(int argc,char**argv)
{
    return 0;
}
4、设置工程链接的库:ws2_32.lib(注意别忘了同时设置Debug和Release生成的时候链接这个库)
DBGWS2_32.PNG RLSWS2_32.PNG
注:这一步可以用一行代码【#pragma comment(lib,"ws2_32.lib")//不需要加分号】代替。

5、因为我认为使用Winsock很自然需要用到多线程,因此我设置使用多线程的VC运行库(注意别忘了同时设置Debug和Release使用多线程库,注意Debug要选择Debug版,Release不要选择Debug版)
DBGMT.PNG RLSMT.PNG

6、因为是个很小的工程,所以我们不需要预编译头。
NOPCH.PNG

现在开始讲Winsock的详细教程。

Windows下在使用Winsock前你必须初始化Winsock,通过调用函数WSAStartup()来完成初始化。Windows下Winsock出现任何错误都可以用WSAGetLastError取得错误的错误代码。
WSAStartup这个函数的特点:
1、不调用它,你就用不了其它所有的Winsock函数。
2、调用它需要提供一个WSADATA结构体来接收Winsock的一些信息比如版本号什么的。
3、调用成功返回1,否则返回0.
原型:
[C] 纯文本查看 复制代码
int WSAStartup(
  WORD wVersionRequested,
  LPWSADATA lpWSAData
);
调用范例:
[C] 纯文本查看 复制代码
WSADATA wsaData;
if(WSAStartup(WINSOCK_VERSION,wsaData))//出错返回非零
{
    fprintf(stderr,"Initialize Winsock failed.\nWSAGetLastError=%u\n",WSAGetLastError());//输出错误信息到stderr
    return 1;//返回1
}
当然,和它对应的收尾函数是WSACleanup()。用完Winsock之后记得调用它来结束你对Winsock的使用。不调用它的话,你的程序退出后可能会暂时引起整个系统的Winsock的紊乱(比如玩游戏无法联机等问题)。比较蛋疼。
大家可能会问我为什么要返回1?嘛,那是因为我假定这个函数是写在int main里面的,通常情况下main函数返回0表示整个程序都成功运行了。那么这里因为出错了所以自然返回一个非零值啦。
WSACleanup()的原型:
[C] 纯文本查看 复制代码
int  WSACleanup (void);
直接调用WSACleanup();就能完成对它的调用。

刚才只是讲了Winsock的初始化与释放。现在来介绍Winsock联网常用的两个协议:TCP/IP协议与UDP协议。
这两个协议采用不同的数据传输方式,一种是“有保障的”,一种是“没有保障的”。其中TCP/IP是“有保障的”,而UDP是“没有保障的”。

UDP教程:
UDP的概述:
UDP协议被称作“数据报”,因为你用它发送数据的时候不需要连接。假设111.222.33.44这个IP地址有台电脑,我要发送一个数据包到这个电脑上,我不需要连接它,我直接发送就行。而那个电脑如果不是接受数据的状态的话,它就会错过这个数据包,从而导致数据包丢失。可以看出UDP是很直接的传输方式。Winsock设置的默认的缓冲区大小是8 KB,如果我发送16 KB的数据包出去,那么事实上只有前面8 KB的数据被发送了出去,后面的都没有发送出去。UDP一次只能传输8 KB的数据包,而且不能保证对方一定能接收到数据包。这就是所谓的“没有保障的”。

TCP/IP就和UDP不一样,TCP/IP在发送数据包以前必须先建立连接,只有成功建立连接到对方电脑后,才能互相收发数据。但是就算你在发送数据的时候,对方电脑并没有处于接受数据的状态,数据也不会丢失,只要对方电脑开始接受数据,我们的数据包就能传达到对方电脑里,可以保障数据包不容易丢失。这就是所谓的“有保障的”。但是TCP/IP比UDP略慢。我觉得“有保障的”比较适合新手入门。

大致说一下UDP方式怎么进行数据传输:
1、电脑A创建套接字,电脑B创建套接字
2、电脑A设置好自己要使用的端口号,电脑B设置成一样的端口号。
3、电脑A准备接收数据,然后电脑B往电脑A发送数据,电脑A收到,或者电脑B准备接收数据,然后电脑A往电脑B发送数据,电脑B收到。
4、传输完了以后,关闭套接字。

因为UDP不需要事先建立连接,因此UDP的发送方式是很自由的。
UDP_.GIF

首先我们需要建立一个叫“套接字”的东西。我们要借助它进行数据的发送与接收。
套接字是SOCKET类型(其实Windows下是int类型,但是请大家声明为SOCKET类型以保证可移植性)。
产生新的套接字是通过调用socket函数或accept函数。这里是UDP教程,accept是TCP/IP的函数,因此请先无视它。
socket的原型:
[C] 纯文本查看 复制代码
SOCKET socket(
  int af,       
  int type,     
  int protocol  
);
参数说明:
1、int af这里请传入AF_INET这个值。你不需要传入别的值。
2、int type这里有个选项:SOCK_STREAM或SOCK_DGRAM。其中SOCK_STREAM指的是具有连接性质的协议比如TCP/IP协议,而SOCK_DGRAM指的是数据报性质的协议比如UDP协议。请传入SOCK_DGRAM这个值,因为这里我是在介绍UDP的玩法。
3、int protocol这里是选择特定协议的。如果你不设置协议类型的话(也就是传入0)也可以。其实我觉得没必要非得指定我们用什么协议,反正type参数已经指明了我们想要的连接方式。
范例代码:
[C] 纯文本查看 复制代码
SOCKET sockMain=socket(AF_INET,SOCK_DGRAM,0);//UDP
if(INVALID_SOCKET==sockMain)
{
    fprintf(stderr,"Create SOCKET failed.\nWSAGetLastError=%u\n",WSAGetLastError());//输出错误信息到stderr
    return 1;//返回1
}
当然SOCKET这种东西创建出来也是要释放掉的,所以请大家在不需要使用SOCKET的时候释放它。通过调用closesocket()来释放它。
closesocket的原型:
[C] 纯文本查看 复制代码
int closesocket(
  SOCKET s  
);
刚才也只是讲了如何建立一个UDP的SOCKET,现在开始讲如何收发数据包。
接收数据包使用recvfrom,发送数据包使用sendto。先来看看函数原型:
sendto函数:
[C] 纯文本查看 复制代码
int sendto(
  SOCKET s,
  const char FAR *buf,
  int len,
  int flags,
  const struct sockaddr FAR *to,
  int tolen
);
recvfrom函数:
[C] 纯文本查看 复制代码
int recvfrom(
  SOCKET s,
  char FAR* buf,
  int len,
  int flags,
  struct sockaddr FAR *from,
  int FAR *fromlen
);
现在来解释一下sendto函数的各个参数的使用:
1、SOCKET s指定你要发送数据所使用的套接字。
2、const char FAR *buf这个是要发送的缓冲区的指针。
3、int len要发送的字节数。
4、int flags设置发送的时候的一些额外的要求,这里只能有两个选项:0(没有别的要求),MSG_DONTROUTE(不要让数据包经过路由。这个要求可能会被网卡驱动无视掉。)
5、const struct sockaddr FAR *to这个参数是一个SOCKADDR结构体的指针,作用是告诉系统你要把数据包发往哪里。SOCKADDR结构体的作用就是指定一个端口和IP地址。
6、int tolen这个参数指定了SOCKADDR结构体的大小。请传入sizeof(SOCKADDR_IN)
recvfrom函数的各个参数的使用:
1、SOCKET s指定你要接收数据所使用的套接字。
2、const char FAR *buf这个是用来接收数据的缓冲区的指针。
3、int len指定缓冲区的大小,换句话说就是你这次最大能接收多少个字节。这个值最好不要大于8192因为Winsock默认的缓冲区大小就是8192字节,你这个值再大你也接收不了那么多信息。
4、int flags设置你接收数据时的一些额外的要求,这里只能有两个选项:0(没有别的要求),MSG_PEEK(不要把数据从缓冲区中删除,函数返回你应该接收的数据包的字节数)
5、struct sockaddr FAR *from这个参数指向一个空的SOCKADDR结构体,然后你就可以通过这个结构体来判断数据是从哪里发来的。可以为NULL
6、int FAR *fromlen这个参数指向一个int指针,获取你提供的SOCKADDR结构体的大小同时告诉你返回的SOCKADDR结构体的大小。如果上一个参数是NULL,这个参数也必须是NULL
大家可以注意到如果你直接调用recvfrom函数,你并没有指定你这个套接字的端口号,因此你无法接收到数据。因此你需要绑定端口号。请使用bind函数来绑定端口号。
bind函数原型:
[C] 纯文本查看 复制代码
int bind(
  SOCKET s,
  const struct sockaddr FAR *name,
  int namelen
);
bind函数的各个参数的作用:
1、SOCKET s指定你要绑定IP地址和端口号的套接字。
2、const struct sockaddr FAR *name指定你要绑定的端口号和IP地址。
3、int namelen指定你给出的SOCKADDR结构体的大小。
sendto函数会在发送数据包的时候自动给你的套接字绑定IP地址和端口号,所以调用过sendto函数的SOCKET就不需要bind了。
当你调用recvfrom函数的时候,这个函数会一直卡住不动,直到有人用sendto发送数据包到你这里,你的recvfrom才会返回。因此Winsock适合多线程编程。

我来画个图告诉大家UDP协议的使用流程。
UDP.GIF
可以看出UDP不需要连接。
细节:bind和sendto都能给SOCKET绑定IP地址和端口。
recvfrom函数在运行的时候会一直等数据包的到来,直到数据包到来了,它才返回。而sendto则会立即返回。
当你在调用recvfrom的时候,无论你准备了多大的缓冲区,recvfrom只要收到了数据包就一定会立即返回。
当你调用sendto发送超过8 KB的数据包的时候,只有前8 KB的数据被成功发送。后面的数据都丢失了。
当一方发送数据的时候如果另一方并没有调用recvfrom,那么另一方将无法接收到数据包。
就像下图这样:
UDP2.GIF

调用sendto、recvfrom和bind这些函数的时候你需要传入一个结构体指针叫“SOCKADDR*”,这东西怎么弄呢?请看下面的代码:
[C] 纯文本查看 复制代码
SOCKADDR_IN sAddr;//我们使用SOCKADDR_IN而不是SOCKADDR
memset(&sAddr,0,sizeof(sAddr));//先把它清零
sAddr.sin_family=AF_INET;//必须设置这个成员而且值必须是AF_INET。
sAddr.sin_port=htons(端口号);//htons负责把一个short转换成Big-Endian,类似的函数还有htonl(把long转换成Big-Endian)
sAddr.sin_addr.s_addr=htonl(IP地址);//IP地址是一个DWORD类型值,举个例,127.0.0.1这个IP地址用DWORD表示就是0x7F000001。
//搞定
IP地址和端口号都是按照Big-Endian存储的。
得到IP地址的方法有很多,一个比较简单的方法是调用inet_addr函数,举例:
sAddr.sin_addr.s_addr=inet_addr("127.0.0.1");其中inet_addr函数返回0x0100007F(inet_addr会自动帮你弄成Big-Endian)

这里给出代码来实际演示一下一个简单的应答程序。
玩法:
按下Ctrl+T,输入IP地址然后按下回车设置自己的发送目标,然后输入字符串按下Ctrl+Z发送。
这个程序会自动接收信息并显示。
按下Ctrl+C结束程序。
[C] 纯文本查看 复制代码
#include<stdio.h>
#include<errno.h>
#include<signal.h>
#include<process.h>
#include<winsock2.h>

//定义自己的宏来保证可移植性

//在Windows平台使用Winsock
#define USE_WSAAPI

//如果出错,输出errno信息
#define USE_ERRNO

const   u_short     g_sPort=11037;//用这个数字做端口号
        char        g_Exit=0;//全程退出时将其置1
        SOCKET      g_sockMain=INVALID_SOCKET;//全局只使用一个套接字
        SOCKADDR_IN g_saCurTarget={0};//当前的发送目标

//============================================================================
//出错后调用这个函数输出错误原因到stderr
//参数:出错的附加信息
//如果定义了USE_ERRNO,这个函数还会输出stderr的错误信息
//如果定义了USE_WSAAPI,这个函数还会输出WSAGetLastError的错误代号
//============================================================================
void ErrOut(const char*szErr,int iRet)
{
    fputs(szErr,stderr);
    fprintf(stderr,"(Function returned %d)\n",iRet);
#   ifdef USE_ERRNO
    fprintf(stderr,"errno=%d(%s)\n",errno,strerror(errno));//先输出errno,因为我可以确定errno不会影响到WSAGetLastError
#   endif
#   ifdef USE_WSAAPI
    fprintf(stderr,"WSAGetLastError=%u\n",WSAGetLastError());
#   endif
}

//============================================================================
//初始化Winsock的函数
//不需要参数
//返回0表示初始化失败,返回1表示初始化成功
//============================================================================
int InitWinsock(void)
{
#   ifdef USE_WSAAPI//Windows下需要调用WSAStartup进行初始化
    int iRet;
    WSADATA wsaData;
    iRet=WSAStartup(WINSOCK_VERSION,&wsaData);//WSAStartup失败返回非零
    if(iRet)
    {
        ErrOut("\"WSAStartup\" failed.",iRet);
        return 0;//出错返回0
    }
#   endif
    return 1;//成功返回1
}

//============================================================================
//关闭Winsock的函数
//返回0表示关闭失败,返回1表示关闭成功
//============================================================================
int ShutdownWinsock(void)
{
#   ifdef USE_WSAAPI//Windows下需要调用WSAStartup进行关闭操作
    int iRet;
    iRet=WSACleanup();//WSACleanup失败返回非零
    if(iRet)
    {
        ErrOut("\"WSACleanup\" failed.",iRet);
        return 0;//出错返回0
    }
#   endif
    return 1;//成功返回1
}

//============================================================================
//线程处理程序:专门负责接收数据包并显示
//============================================================================
void ReceiverThread(void*p)
{
    char szBuf[0x2000];//接收缓冲区
    SOCKADDR_IN sAddr={0};
    int iFromLen=sizeof(sAddr);
    int iRet=0;

    printf("Now Data Receiver is able to accept data from port %u.\n",g_sPort);

    sAddr.sin_family=AF_INET;
    sAddr.sin_port=htons(g_sPort);
    sAddr.sin_addr.s_addr=htonl(INADDR_ANY);//INADDR的宏都是LE的,因此需要用htonl
    iRet=bind(g_sockMain,(SOCKADDR*)&sAddr,sizeof(sAddr));//先绑定端口号
    if(iRet)
    {
        ErrOut("\"ReceiverThread\" failed to \"bind\".",iRet);
        return;
    }

    memset(szBuf,0,sizeof(szBuf));
    iRet=recvfrom(g_sockMain,szBuf,sizeof(szBuf),0,(SOCKADDR*)&sAddr,&iFromLen);
    while(iRet>0)
    {
        printf("Received from:%d.%d.%d.%d\n%s\n",//先打印来源然后打印内容
            sAddr.sin_addr.s_net,//IP地址
            sAddr.sin_addr.s_host,
            sAddr.sin_addr.s_lh,
            sAddr.sin_addr.s_impno,
            szBuf);
        if(g_Exit)
        {
            printf("Data Receiver is stopped.\n");
            return;
        }
        iFromLen=sizeof(sAddr);
        memset(szBuf,0,sizeof(szBuf));
        iRet=recvfrom(g_sockMain,szBuf,sizeof(szBuf),0,(SOCKADDR*)&sAddr,&iFromLen);
    }
    ErrOut("recvfrom failed.",iRet);
}

//============================================================================
//信号处理程序:用于处理Ctrl+C
//============================================================================
void SignalProc(int iSignal)
{
    switch(iSignal)
    {
        case SIGINT:
        {
            SOCKADDR_IN sSelf={0};//自己给自己发送消息来使接收消息的线程继续运行。
            char szQuit[]="Ctrl+C detected. Quitting.\n";
            if(g_sockMain!=INVALID_SOCKET)
            {
                int iRet;
                sSelf.sin_family=AF_INET;
                sSelf.sin_port=htons(g_sPort);
                sSelf.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
                iRet=sendto(g_sockMain,szQuit,sizeof(szQuit),0,(SOCKADDR*)&sSelf,sizeof(sSelf));
                if(iRet<=0)
                    ErrOut("\"sendto\" failed.",iRet);
            }
            g_Exit=1;
            fflush(stdin);
            puts(szQuit);
        }
        break;
    }
}

//============================================================================
//整个程序的入口点
//目前不打算把程序的运行结果输出到ERRORLEVEL
//也不打算使用参数
//============================================================================
void main(void)//这个程序不使用参数。
{
    char szBuf[0x2000];
    size_t nInputLen=0;
    int iRet;
    puts(
        "First you need to press CTRL+T, input an IP address, press ENTER, and press CTRL+Z to specify your target IP address.\n"
        "Then you can input any text data and press CTRL+Z to send this string to your target.\n"
        "Press CTRL+C to exit this program.\n"
        "You should to specify the address first.");//Ctrl+T会获得控制字符0x14
    signal(SIGINT,SignalProc);//设置Ctrl+C的信号处理程序
    if(InitWinsock())//初始化Socket
    {
        g_sockMain=socket(AF_INET,SOCK_DGRAM,0);//创建主要的套接字
        if(g_sockMain==INVALID_SOCKET)
        {
            ErrOut("\"socket\" failed.",g_sockMain);//创建失败的处理
            ShutdownWinsock();
            return;
        }
        _beginthread(ReceiverThread,0,(void*)g_sockMain);//建立一个新线程来接收发来的数据包
        do
        {
            nInputLen=fread(szBuf,1,sizeof(szBuf),stdin);//从stdin读取数据
            if(szBuf[0]==0x14)//第一个字符如果是Ctrl+T
            {
                char *pEnd;
                pEnd=strchr(szBuf+1,'\r');
                if(pEnd)*pEnd=0;
                pEnd=strchr(szBuf+1,'\n');
                if(pEnd)*pEnd=0;
                g_saCurTarget.sin_family=AF_INET;
                g_saCurTarget.sin_port=htons(g_sPort);
                g_saCurTarget.sin_addr.s_addr=inet_addr(szBuf+1);//设置IP地址
                printf("Target IP=%d.%d.%d.%d\n",
                    g_saCurTarget.sin_addr.s_net,
                    g_saCurTarget.sin_addr.s_host,
                    g_saCurTarget.sin_addr.s_lh,
                    g_saCurTarget.sin_addr.s_impno);
            }
            else//否则发送数据
            {
                if(g_saCurTarget.sin_family!=AF_INET)//如果没设置IP地址
                {
                    fputs("You need to specify a target first.\n"//提示要设置IP地址
                        "Please press CTRL+T, input an IP address, press ENTER, and press CTRL+Z to specify your target IP address.",stderr);
                }
                else
                {
                    szBuf[nInputLen]=0;//设置文本结尾的\0
                    iRet=sendto(g_sockMain,szBuf,nInputLen,0,(SOCKADDR*)&g_saCurTarget,sizeof(g_saCurTarget));//发送
                    if(iRet<=0)
                        ErrOut("\"sendto\" failed.",iRet);
                }
            }
        }while(!g_Exit);
        ShutdownWinsock();
    }
}
Chat.PNG
实例下载地址:
游客,如果您要查看本帖隐藏内容请回复
TCP/IP教程:
TCP/IP和UDP的区别在于TCP/IP多了一个连接的过程。

TCP/IP进行数据传输的方式:
1、电脑A建立套接字,电脑B也建立套接字
2、电脑A设置好端口,等待别的电脑去连接电脑A。
3、电脑B连接电脑A
4、电脑B准备接受数据,电脑A发送数据,电脑B收到,或者电脑A准备接受数据,电脑B发送数据,电脑A收到
5、传输完了以后,关闭套接字(其中一方关闭套接字就会导致连接断开。)

其中,等待别的电脑连入的叫“主机端”,而去连接别的电脑的叫“客户端”。说白了,“客户端”是“攻”,“主机端”是“受”。
我来画个图,演示一下TCP/IP的使用过程。
TCPIP.PNG
可以看出,就算电脑2在发送的时候电脑1并没有准备好要接收数据,电脑1调用recv仍然能接收到数据包。
TCP/IP使用send和recv函数收发数据包。
你可以一次性send超过8 KB的数据包,但是你需要通过分批recv来接收完所有的数据包。
可以通过ioctlsocket函数或给recv函数指定MSG_PEEK来获取待接收数据的数量。
这里给出两个函数的原型:
send的原型:
[C] 纯文本查看 复制代码
int send(
  SOCKET s,              
  const char FAR *buf,  
  int len,               
  int flags              
);
recv的原型:
[C] 纯文本查看 复制代码
int recv(
  SOCKET s,       
  char FAR *buf,  
  int len,        
  int flags       
);
ioctlsocket的原型:
[C] 纯文本查看 复制代码
int ioctlsocket(
  SOCKET s,
  long cmd,
  u_long FAR *argp
);
参数详细信息:
send的参数:
SOCKET s指定你用来发送的套接字。
const char FAR *buf指定你要发送的数据的指针。
int len你要发送的数据的长度。
int flags你的额外要求。可选:0(没有别的要求),MSG_OOB(不通过主线发送),MSG_DONTROUTE(数据包不经过路由,这个要求可能会被网卡驱动无视)
recv的参数:
SOCKET s指定你用来发送的套接字。
const char FAR *buf指定你要发送的数据的指针。
int len你要发送的数据的长度。
int flags你的额外要求。可选:0(没有别的要求),MSG_OOB(接收不通过主线发送的数据),MSG_PEEK(不从缓冲区中删除数据)
ioctlsocket的参数:
SOCKET s指定你用来发送的套接字。
long cmd你的命令,可选择的值:FIONBIO(使套接字的函数不会卡住),FIONREAD(待接收的数据包大小),SIOCATMARK(检查有没有不通过主线发送的数据)
这里介绍一个概念:OOB数据包
所谓OOB(Out-Of-Band)指的是,你通过OOB方式发送的数据包必须通过OOB方式接收。你不通过OOB发送的数据包则不必用OOB方式接收。
可以看出send和recv都不必指定目标。

光说了如何发送数据包。现在开始讲如何建立连接。
首先,TCP/IP要区分“客户机”和“主机”
主机先建立套接字(socket函数),然后用bind函数绑定端口号,接着调用listen函数使主机进入侦听模式,最后调用accept函数等待客户机连接到主机。
客户机建立套接字(socket函数),然后调用connect函数向主机发起连接。注意主机必须在客户机调用connect之前调用accept,否则主机无法接收到客户机的连接请求。

accept是一个会卡住的函数(Blocking call)。调用它后,它会一直卡住,直到有客户端连接。
客户端连接的时候,accept会返回一个SOCKET,同时返回客户端的IP地址。这个时候主机端就可以通过这个返回的SOCKET和客户机进行交互。
accept函数原型如下:
[C] 纯文本查看 复制代码
SOCKET accept(
  SOCKET s,
  struct sockaddr FAR *addr,
  int FAR *addrlen
);
参数信息:
SOCKET s指定侦听中的套接字。
struct sockaddr FAR *addr返回客户机的IP地址和端口,可以为NULL
int FAR *addrlen返回客户机IP地址和端口那个结构体的大小……(很绕口,大致意思就是上面那个struct sockaddr FAR *addr的大小了)如果上一个参数是NULL,这个参数也必须是NULL

accept是受,那么connect是攻。
connect的原型:
[C] 纯文本查看 复制代码
int connect(
  SOCKET s,
  const struct sockaddr FAR *name,
  int namelen
);
connect会立即返回,不会卡住。
参数信息:
SOCKET s指定你用来连接的套接字
const struct sockaddr FAR *name指定你的连接目标
int namelen指定上面那个结构体的大小。

因为accept函数返回的套接字是主机端和客户端进行通讯用的套接字,因此主机端可以同时和多个客户端进行通讯。
下面我画张图来演示这样的关系。
树形.GIF
主机端建立的套接字只是用来让客户机来连接。
这样一说就明白了吧?

最后放上代码:C语言通过Winsock下载网页内容的代码。
[C] 纯文本查看 复制代码
#include<stdio.h>
#include<winsock2.h>

#define BUF_SIZE 0x2000

int main(int argc,char**argv)
{
    WSADATA wsaData;
    char szUrl[0x1000]={0};
    char szBuf[BUF_SIZE+1]={0};
    if(WSAStartup(WINSOCK_VERSION,&wsaData))
    {
        fputs("初始化Winsock失败。\n",stderr);
        return 1;
    }
    for(;;)
    {
        char *pDir=NULL;
        struct hostent* hAddr=NULL;
        SOCKADDR_IN sAddr={0};
        SOCKET sConnect=INVALID_SOCKET;
        int nSendLen=0;
        int nRecv=0;
        printf("请输入网址。\n");
        gets(szUrl);
        if(!strlen(szUrl))
        {
            fputs("没有输入网址,程序退出。\n",stderr);
            goto BadEnd;
        }
        if(strnicmp(szUrl,"http://",7))
        {
            fputs("请输入以http://开头的网址……\n",stderr);
            continue;
        }
        if(pDir=strchr(szUrl+7,'/'))
        {
            *pDir++=0;
            printf("网址:%s\n目录:%s\n",szUrl+7,pDir);
        }
        else
            printf("网址:%s\n",szUrl+7);
        hAddr=gethostbyname(szUrl+7);
        if(!hAddr)
        {
            fprintf(stderr,"域名解析失败。\nWSAGetLastError=%u\n",WSAGetLastError());
            continue;
        }
        printf("目标IP地址:%u.%u.%u.%u\n",
            (unsigned char)(hAddr->h_addr_list[0][0]),
            (unsigned char)(hAddr->h_addr_list[0][1]),
            (unsigned char)(hAddr->h_addr_list[0][2]),
            (unsigned char)(hAddr->h_addr_list[0][3]));
        sAddr.sin_family=AF_INET;
        sAddr.sin_port=htons(80);
        sAddr.sin_addr.s_addr=*(u_long*)(hAddr->h_addr_list[0]);
        sConnect=socket(AF_INET,SOCK_STREAM,0);
        if(sConnect==INVALID_SOCKET)
        {
            fprintf(stderr,"无法创建SOCKET套接字。\nWSAGetLastError=%u\n",WSAGetLastError());
            goto BadEnd;
        }
        if(connect(sConnect,(SOCKADDR*)&sAddr,sizeof(sAddr)))
        {
            fprintf(stderr,"无法连接。\nWSAGetLastError=%u\n",WSAGetLastError());
            closesocket(sConnect);
            continue;
        }
        if(!pDir)
            pDir="";
        sprintf(szBuf,
            "GET /%s HTTP/1.1\n"
            "Accept: text/html, application/xhtml+xml, */*\n"
            "Accept-Language: zh-CN\n"
            "User-Agent: Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko\n"
            //"Accept-Encoding: gzip, deflate\n"
            "Host: %s\n"
            "DNT: 1\n"
            "Connection: Keep-Alive\n"
            "\n"
            ,pDir,szUrl+7);
        nSendLen=strlen(szBuf)+1;
        if(send(sConnect,szBuf,nSendLen,0)!=nSendLen)
            fprintf(stderr,"不能完整发送数据包。\nWSAGetLastError=%u\n",WSAGetLastError());
        do
        {
            memset(szBuf,0,BUF_SIZE);
            nRecv=recv(sConnect,szBuf,BUF_SIZE,0);
                        if(nRecv==SOCKET_ERROR)
                                nRecv=BUF_SIZE;
                        fwrite(szBuf,1,nRecv,stdout);
            printf("%s",szBuf);
        }while(nRecv==BUF_SIZE);
                puts("\n");
        closesocket(sConnect);
    }
    WSACleanup();
    return 0;
BadEnd:
    WSACleanup();
    return 1;
}
代码下载地址:
游客,如果您要查看本帖隐藏内容请回复


本帖被以下淘专辑推荐:

34

主题

138

帖子

7100

积分

用户组: 管理员

UID
77
精华
11
威望
112 点
宅币
6554 个
贡献
129 次
宅之契约
0 份
在线时间
100 小时
注册时间
2014-2-22
发表于 2014-3-17 10:18:11 | 显示全部楼层
本帖最后由 美俪女神 于 2014-3-21 11:21 编辑

说实话,LZ的文章写得好,例子实在不够清晰。
下面附上我的例子,本机、局域网、公网均测试成功。
UDP: v0.rar (52.21 KB, 下载次数: 52)

点评

屌!  详情 回复 发表于 2014-3-21 14:43
路过膜拜大牛  详情 回复 发表于 2014-3-21 08:38

4

主题

29

帖子

285

积分

用户组: 版主

UID
32
精华
0
威望
6 点
宅币
244 个
贡献
0 次
宅之契约
0 份
在线时间
45 小时
注册时间
2014-2-7
发表于 2014-3-17 06:58:36 | 显示全部楼层
系统自动沙发

25

主题

114

帖子

1134

积分

用户组: 管理员

UID
113
精华
15
威望
31 点
宅币
854 个
贡献
29 次
宅之契约
0 份
在线时间
96 小时
注册时间
1970-1-1
发表于 2014-3-21 08:38:07 | 显示全部楼层
美俪女神 发表于 2014-3-17 10:18
说实话,LZ的文章写得好,例子实在不够清晰。
下面附上我的例子,本机、局域网、公网均测试成功。
UDP:( ...

路过膜拜大牛
*0xAA55站SysOp
任何疑问,直接联系A5

1028

主题

2283

帖子

5万

积分

用户组: 管理员

一只技术宅

UID
1
精华
201
威望
271 点
宅币
17523 个
贡献
35220 次
宅之契约
0 份
在线时间
1676 小时
注册时间
2014-1-26
 楼主| 发表于 2014-3-21 14:43:11 | 显示全部楼层
美俪女神 发表于 2014-3-17 02:18
说实话,LZ的文章写得好,例子实在不够清晰。
下面附上我的例子,本机、局域网、公网均测试成功。
UDP:( ...

屌!

34

主题

138

帖子

7100

积分

用户组: 管理员

UID
77
精华
11
威望
112 点
宅币
6554 个
贡献
129 次
宅之契约
0 份
在线时间
100 小时
注册时间
2014-2-22
发表于 2014-3-22 15:32:38 | 显示全部楼层

帮我顶下这个帖子吧:http://www.0xaa55.com/thread-392-1-1.html
光有人下载木有人回复。

0

主题

19

帖子

51

积分

用户组: 小·技术宅

UID
158
精华
0
威望
1 点
宅币
30 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2014-3-26
发表于 2014-3-26 16:27:54 | 显示全部楼层
路过。学习了

4

主题

48

帖子

149

积分

用户组: 小·技术宅

UID
208
精华
0
威望
1 点
宅币
97 个
贡献
0 次
宅之契约
0 份
在线时间
7 小时
注册时间
2014-4-16
发表于 2014-4-18 18:31:21 | 显示全部楼层
看着这么多的源码,感觉好爽

0

主题

2

帖子

23

积分

用户组: 初·技术宅

UID
238
精华
0
威望
1 点
宅币
19 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2014-4-30
发表于 2014-5-3 12:09:20 | 显示全部楼层
.相见恨晚啊!

0

主题

1

帖子

16

积分

用户组: 初·技术宅

UID
255
精华
0
威望
1 点
宅币
13 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2014-5-6
发表于 2014-5-6 09:53:00 | 显示全部楼层
相见恨晚                                                                           

0

主题

7

帖子

44

积分

用户组: 初·技术宅

UID
62
精华
0
威望
1 点
宅币
35 个
贡献
0 次
宅之契约
0 份
在线时间
3 小时
注册时间
2014-2-15
发表于 2014-5-6 22:51:45 | 显示全部楼层
最近刚刚学这个,谢谢作者

0

主题

14

帖子

71

积分

用户组: 小·技术宅

UID
109
精华
0
威望
1 点
宅币
55 个
贡献
0 次
宅之契约
0 份
在线时间
6 小时
注册时间
2014-3-7
发表于 2014-5-9 14:45:01 | 显示全部楼层
神啊,终于让我找到了!

0

主题

12

帖子

36

积分

用户组: 初·技术宅

UID
385
精华
0
威望
1 点
宅币
22 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2014-7-18
发表于 2014-7-18 14:51:03 | 显示全部楼层
Winsock在Windows下的编程教程

0

主题

5

帖子

25

积分

用户组: 初·技术宅

UID
406
精华
0
威望
1 点
宅币
18 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2014-8-1
发表于 2014-8-1 17:17:38 | 显示全部楼层
很好的例子。

7

主题

21

帖子

113

积分

用户组: 小·技术宅

UID
245
精华
0
威望
2 点
宅币
86 个
贡献
2 次
宅之契约
0 份
在线时间
6 小时
注册时间
2014-5-3
发表于 2014-8-6 19:57:37 | 显示全部楼层
漏主的井绳是值得学习的
卡卡 该用户已被删除
发表于 2015-6-12 16:59:11 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽

0

主题

76

帖子

6735

积分

用户组: 真·技术宅

UID
604
精华
0
威望
1 点
宅币
804 个
贡献
5853 次
宅之契约
0 份
在线时间
97 小时
注册时间
2014-12-20
发表于 2015-6-13 11:48:06 | 显示全部楼层
外加上各种字符集..也是够蛋疼的.

0

主题

10

帖子

9

积分

用户组: 初·技术宅

UID
952
精华
0
威望
0 点
宅币
-1 个
贡献
0 次
宅之契约
0 份
在线时间
1 小时
注册时间
2015-7-12
发表于 2015-7-14 20:01:46 | 显示全部楼层
啊啊啊啊啊啊啊啊啊啊啊啊啊啊

0

主题

2

帖子

28

积分

用户组: 初·技术宅

UID
1009
精华
0
威望
1 点
宅币
24 个
贡献
0 次
宅之契约
0 份
在线时间
3 小时
注册时间
2015-8-2
发表于 2015-8-25 15:05:55 | 显示全部楼层
不错的教程 复习下

0

主题

2

帖子

15

积分

用户组: 初·技术宅

UID
1111
精华
0
威望
1 点
宅币
11 个
贡献
0 次
宅之契约
0 份
在线时间
0 小时
注册时间
2015-9-4
发表于 2015-9-4 09:45:55 | 显示全部楼层
求学习!!!!!!!!!!!!!!!!!!!
回复

使用道具 举报

本版积分规则

QQ|申请友链||Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号|网站地图

GMT+8, 2019-4-20 22:49 , Processed in 0.148703 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表