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

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 40461|回复: 45

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

[复制链接]
发表于 2014-3-16 22:57:19 | 显示全部楼层 |阅读模式

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

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

×
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、快速敲入以下代码为后面的教程做好准备:
  1. #include<stdio.h>
  2. #include<winsock2.h>

  3. int main(int argc,char**argv)
  4. {
  5.     return 0;
  6. }
复制代码
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.
原型:
  1. int WSAStartup(
  2.   WORD wVersionRequested,
  3.   LPWSADATA lpWSAData
  4. );
复制代码
调用范例:
  1. WSADATA wsaData;
  2. if(WSAStartup(WINSOCK_VERSION,wsaData))//出错返回非零
  3. {
  4.     fprintf(stderr,"Initialize Winsock failed.\nWSAGetLastError=%u\n",WSAGetLastError());//输出错误信息到stderr
  5.     return 1;//返回1
  6. }
复制代码
当然,和它对应的收尾函数是WSACleanup()。用完Winsock之后记得调用它来结束你对Winsock的使用。不调用它的话,你的程序退出后可能会暂时引起整个系统的Winsock的紊乱(比如玩游戏无法联机等问题)。比较蛋疼。
大家可能会问我为什么要返回1?嘛,那是因为我假定这个函数是写在int main里面的,通常情况下main函数返回0表示整个程序都成功运行了。那么这里因为出错了所以自然返回一个非零值啦。
WSACleanup()的原型:
  1. 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的原型:
  1. SOCKET socket(
  2.   int af,      
  3.   int type,     
  4.   int protocol  
  5. );
复制代码
参数说明:
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参数已经指明了我们想要的连接方式。
范例代码:
  1. SOCKET sockMain=socket(AF_INET,SOCK_DGRAM,0);//UDP
  2. if(INVALID_SOCKET==sockMain)
  3. {
  4.     fprintf(stderr,"Create SOCKET failed.\nWSAGetLastError=%u\n",WSAGetLastError());//输出错误信息到stderr
  5.     return 1;//返回1
  6. }
复制代码
当然SOCKET这种东西创建出来也是要释放掉的,所以请大家在不需要使用SOCKET的时候释放它。通过调用closesocket()来释放它。
closesocket的原型:
  1. int closesocket(
  2.   SOCKET s  
  3. );
复制代码
刚才也只是讲了如何建立一个UDP的SOCKET,现在开始讲如何收发数据包。
接收数据包使用recvfrom,发送数据包使用sendto。先来看看函数原型:
sendto函数:
  1. int sendto(
  2.   SOCKET s,
  3.   const char FAR *buf,
  4.   int len,
  5.   int flags,
  6.   const struct sockaddr FAR *to,
  7.   int tolen
  8. );
复制代码
recvfrom函数:
  1. int recvfrom(
  2.   SOCKET s,
  3.   char FAR* buf,
  4.   int len,
  5.   int flags,
  6.   struct sockaddr FAR *from,
  7.   int FAR *fromlen
  8. );
复制代码
现在来解释一下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函数原型:
  1. int bind(
  2.   SOCKET s,
  3.   const struct sockaddr FAR *name,
  4.   int namelen
  5. );
复制代码
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*”,这东西怎么弄呢?请看下面的代码:
  1. SOCKADDR_IN sAddr;//我们使用SOCKADDR_IN而不是SOCKADDR
  2. memset(&sAddr,0,sizeof(sAddr));//先把它清零
  3. sAddr.sin_family=AF_INET;//必须设置这个成员而且值必须是AF_INET。
  4. sAddr.sin_port=htons(端口号);//htons负责把一个short转换成Big-Endian,类似的函数还有htonl(把long转换成Big-Endian)
  5. sAddr.sin_addr.s_addr=htonl(IP地址);//IP地址是一个DWORD类型值,举个例,127.0.0.1这个IP地址用DWORD表示就是0x7F000001。
  6. //搞定
复制代码
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结束程序。
  1. #include<stdio.h>
  2. #include<errno.h>
  3. #include<signal.h>
  4. #include<process.h>
  5. #include<winsock2.h>

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

  7. //在Windows平台使用Winsock
  8. #define USE_WSAAPI

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

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

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

  32. //============================================================================
  33. //初始化Winsock的函数
  34. //不需要参数
  35. //返回0表示初始化失败,返回1表示初始化成功
  36. //============================================================================
  37. int InitWinsock(void)
  38. {
  39. #   ifdef USE_WSAAPI//Windows下需要调用WSAStartup进行初始化
  40.     int iRet;
  41.     WSADATA wsaData;
  42.     iRet=WSAStartup(WINSOCK_VERSION,&wsaData);//WSAStartup失败返回非零
  43.     if(iRet)
  44.     {
  45.         ErrOut(""WSAStartup" failed.",iRet);
  46.         return 0;//出错返回0
  47.     }
  48. #   endif
  49.     return 1;//成功返回1
  50. }

  51. //============================================================================
  52. //关闭Winsock的函数
  53. //返回0表示关闭失败,返回1表示关闭成功
  54. //============================================================================
  55. int ShutdownWinsock(void)
  56. {
  57. #   ifdef USE_WSAAPI//Windows下需要调用WSAStartup进行关闭操作
  58.     int iRet;
  59.     iRet=WSACleanup();//WSACleanup失败返回非零
  60.     if(iRet)
  61.     {
  62.         ErrOut(""WSACleanup" failed.",iRet);
  63.         return 0;//出错返回0
  64.     }
  65. #   endif
  66.     return 1;//成功返回1
  67. }

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

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

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

  87.     memset(szBuf,0,sizeof(szBuf));
  88.     iRet=recvfrom(g_sockMain,szBuf,sizeof(szBuf),0,(SOCKADDR*)&sAddr,&iFromLen);
  89.     while(iRet>0)
  90.     {
  91.         printf("Received from:%d.%d.%d.%d\n%s\n",//先打印来源然后打印内容
  92.             sAddr.sin_addr.s_net,//IP地址
  93.             sAddr.sin_addr.s_host,
  94.             sAddr.sin_addr.s_lh,
  95.             sAddr.sin_addr.s_impno,
  96.             szBuf);
  97.         if(g_Exit)
  98.         {
  99.             printf("Data Receiver is stopped.\n");
  100.             return;
  101.         }
  102.         iFromLen=sizeof(sAddr);
  103.         memset(szBuf,0,sizeof(szBuf));
  104.         iRet=recvfrom(g_sockMain,szBuf,sizeof(szBuf),0,(SOCKADDR*)&sAddr,&iFromLen);
  105.     }
  106.     ErrOut("recvfrom failed.",iRet);
  107. }

  108. //============================================================================
  109. //信号处理程序:用于处理Ctrl+C
  110. //============================================================================
  111. void SignalProc(int iSignal)
  112. {
  113.     switch(iSignal)
  114.     {
  115.         case SIGINT:
  116.         {
  117.             SOCKADDR_IN sSelf={0};//自己给自己发送消息来使接收消息的线程继续运行。
  118.             char szQuit[]="Ctrl+C detected. Quitting.\n";
  119.             if(g_sockMain!=INVALID_SOCKET)
  120.             {
  121.                 int iRet;
  122.                 sSelf.sin_family=AF_INET;
  123.                 sSelf.sin_port=htons(g_sPort);
  124.                 sSelf.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
  125.                 iRet=sendto(g_sockMain,szQuit,sizeof(szQuit),0,(SOCKADDR*)&sSelf,sizeof(sSelf));
  126.                 if(iRet<=0)
  127.                     ErrOut(""sendto" failed.",iRet);
  128.             }
  129.             g_Exit=1;
  130.             fflush(stdin);
  131.             puts(szQuit);
  132.         }
  133.         break;
  134.     }
  135. }

  136. //============================================================================
  137. //整个程序的入口点
  138. //目前不打算把程序的运行结果输出到ERRORLEVEL
  139. //也不打算使用参数
  140. //============================================================================
  141. void main(void)//这个程序不使用参数。
  142. {
  143.     char szBuf[0x2000];
  144.     size_t nInputLen=0;
  145.     int iRet;
  146.     puts(
  147.         "First you need to press CTRL+T, input an IP address, press ENTER, and press CTRL+Z to specify your target IP address.\n"
  148.         "Then you can input any text data and press CTRL+Z to send this string to your target.\n"
  149.         "Press CTRL+C to exit this program.\n"
  150.         "You should to specify the address first.");//Ctrl+T会获得控制字符0x14
  151.     signal(SIGINT,SignalProc);//设置Ctrl+C的信号处理程序
  152.     if(InitWinsock())//初始化Socket
  153.     {
  154.         g_sockMain=socket(AF_INET,SOCK_DGRAM,0);//创建主要的套接字
  155.         if(g_sockMain==INVALID_SOCKET)
  156.         {
  157.             ErrOut(""socket" failed.",g_sockMain);//创建失败的处理
  158.             ShutdownWinsock();
  159.             return;
  160.         }
  161.         _beginthread(ReceiverThread,0,(void*)g_sockMain);//建立一个新线程来接收发来的数据包
  162.         do
  163.         {
  164.             nInputLen=fread(szBuf,1,sizeof(szBuf),stdin);//从stdin读取数据
  165.             if(szBuf[0]==0x14)//第一个字符如果是Ctrl+T
  166.             {
  167.                 char *pEnd;
  168.                 pEnd=strchr(szBuf+1,'\r');
  169.                 if(pEnd)*pEnd=0;
  170.                 pEnd=strchr(szBuf+1,'\n');
  171.                 if(pEnd)*pEnd=0;
  172.                 g_saCurTarget.sin_family=AF_INET;
  173.                 g_saCurTarget.sin_port=htons(g_sPort);
  174.                 g_saCurTarget.sin_addr.s_addr=inet_addr(szBuf+1);//设置IP地址
  175.                 printf("Target IP=%d.%d.%d.%d\n",
  176.                     g_saCurTarget.sin_addr.s_net,
  177.                     g_saCurTarget.sin_addr.s_host,
  178.                     g_saCurTarget.sin_addr.s_lh,
  179.                     g_saCurTarget.sin_addr.s_impno);
  180.             }
  181.             else//否则发送数据
  182.             {
  183.                 if(g_saCurTarget.sin_family!=AF_INET)//如果没设置IP地址
  184.                 {
  185.                     fputs("You need to specify a target first.\n"//提示要设置IP地址
  186.                         "Please press CTRL+T, input an IP address, press ENTER, and press CTRL+Z to specify your target IP address.",stderr);
  187.                 }
  188.                 else
  189.                 {
  190.                     szBuf[nInputLen]=0;//设置文本结尾的\0
  191.                     iRet=sendto(g_sockMain,szBuf,nInputLen,0,(SOCKADDR*)&g_saCurTarget,sizeof(g_saCurTarget));//发送
  192.                     if(iRet<=0)
  193.                         ErrOut(""sendto" failed.",iRet);
  194.                 }
  195.             }
  196.         }while(!g_Exit);
  197.         ShutdownWinsock();
  198.     }
  199. }
复制代码
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的原型:
  1. int send(
  2.   SOCKET s,              
  3.   const char FAR *buf,  
  4.   int len,               
  5.   int flags              
  6. );
复制代码
recv的原型:
  1. int recv(
  2.   SOCKET s,      
  3.   char FAR *buf,  
  4.   int len,        
  5.   int flags      
  6. );
复制代码
ioctlsocket的原型:
  1. int ioctlsocket(
  2.   SOCKET s,
  3.   long cmd,
  4.   u_long FAR *argp
  5. );
复制代码
参数详细信息:
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函数原型如下:
  1. SOCKET accept(
  2.   SOCKET s,
  3.   struct sockaddr FAR *addr,
  4.   int FAR *addrlen
  5. );
复制代码
参数信息:
SOCKET s指定侦听中的套接字。
struct sockaddr FAR *addr返回客户机的IP地址和端口,可以为NULL
int FAR *addrlen返回客户机IP地址和端口那个结构体的大小……(很绕口,大致意思就是上面那个struct sockaddr FAR *addr的大小了)如果上一个参数是NULL,这个参数也必须是NULL

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

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

最后放上代码:C语言通过Winsock下载网页内容的代码。
  1. #include<stdio.h>
  2. #include<winsock2.h>

  3. #define BUF_SIZE 0x2000

  4. int main(int argc,char**argv)
  5. {
  6.     WSADATA wsaData;
  7.     char szUrl[0x1000]={0};
  8.     char szBuf[BUF_SIZE+1]={0};
  9.     if(WSAStartup(WINSOCK_VERSION,&wsaData))
  10.     {
  11.         fputs("初始化Winsock失败。\n",stderr);
  12.         return 1;
  13.     }
  14.     for(;;)
  15.     {
  16.         char *pDir=NULL;
  17.         struct hostent* hAddr=NULL;
  18.         SOCKADDR_IN sAddr={0};
  19.         SOCKET sConnect=INVALID_SOCKET;
  20.         int nSendLen=0;
  21.         int nRecv=0;
  22.         printf("请输入网址。\n");
  23.         gets(szUrl);
  24.         if(!strlen(szUrl))
  25.         {
  26.             fputs("没有输入网址,程序退出。\n",stderr);
  27.             goto BadEnd;
  28.         }
  29.         if(strnicmp(szUrl,"http://",7))
  30.         {
  31.             fputs("请输入以http://开头的网址……\n",stderr);
  32.             continue;
  33.         }
  34.         if(pDir=strchr(szUrl+7,'/'))
  35.         {
  36.             *pDir++=0;
  37.             printf("网址:%s\n目录:%s\n",szUrl+7,pDir);
  38.         }
  39.         else
  40.             printf("网址:%s\n",szUrl+7);
  41.         hAddr=gethostbyname(szUrl+7);
  42.         if(!hAddr)
  43.         {
  44.             fprintf(stderr,"域名解析失败。\nWSAGetLastError=%u\n",WSAGetLastError());
  45.             continue;
  46.         }
  47.         printf("目标IP地址:%u.%u.%u.%u\n",
  48.             (unsigned char)(hAddr->h_addr_list[0][0]),
  49.             (unsigned char)(hAddr->h_addr_list[0][1]),
  50.             (unsigned char)(hAddr->h_addr_list[0][2]),
  51.             (unsigned char)(hAddr->h_addr_list[0][3]));
  52.         sAddr.sin_family=AF_INET;
  53.         sAddr.sin_port=htons(80);
  54.         sAddr.sin_addr.s_addr=*(u_long*)(hAddr->h_addr_list[0]);
  55.         sConnect=socket(AF_INET,SOCK_STREAM,0);
  56.         if(sConnect==INVALID_SOCKET)
  57.         {
  58.             fprintf(stderr,"无法创建SOCKET套接字。\nWSAGetLastError=%u\n",WSAGetLastError());
  59.             goto BadEnd;
  60.         }
  61.         if(connect(sConnect,(SOCKADDR*)&sAddr,sizeof(sAddr)))
  62.         {
  63.             fprintf(stderr,"无法连接。\nWSAGetLastError=%u\n",WSAGetLastError());
  64.             closesocket(sConnect);
  65.             continue;
  66.         }
  67.         if(!pDir)
  68.             pDir="";
  69.         sprintf(szBuf,
  70.             "GET /%s HTTP/1.1\n"
  71.             "Accept: text/html, application/xhtml+xml, */*\n"
  72.             "Accept-Language: zh-CN\n"
  73.             "User-Agent: Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko\n"
  74.             //"Accept-Encoding: gzip, deflate\n"
  75.             "Host: %s\n"
  76.             "DNT: 1\n"
  77.             "Connection: Keep-Alive\n"
  78.             "\n"
  79.             ,pDir,szUrl+7);
  80.         nSendLen=strlen(szBuf)+1;
  81.         if(send(sConnect,szBuf,nSendLen,0)!=nSendLen)
  82.             fprintf(stderr,"不能完整发送数据包。\nWSAGetLastError=%u\n",WSAGetLastError());
  83.         do
  84.         {
  85.             memset(szBuf,0,BUF_SIZE);
  86.             nRecv=recv(sConnect,szBuf,BUF_SIZE,0);
  87.                         if(nRecv==SOCKET_ERROR)
  88.                                 nRecv=BUF_SIZE;
  89.                         fwrite(szBuf,1,nRecv,stdout);
  90.             printf("%s",szBuf);
  91.         }while(nRecv==BUF_SIZE);
  92.                 puts("\n");
  93.         closesocket(sConnect);
  94.     }
  95.     WSACleanup();
  96.     return 0;
  97. BadEnd:
  98.     WSACleanup();
  99.     return 1;
  100. }
复制代码
代码下载地址:
游客,如果您要查看本帖隐藏内容请回复


本帖被以下淘专辑推荐:

回复

使用道具 举报

发表于 2014-3-17 10:18:11 | 显示全部楼层
本帖最后由 美俪女神 于 2014-3-21 11:21 编辑

说实话,LZ的文章写得好,例子实在不够清晰。
下面附上我的例子,本机、局域网、公网均测试成功。
UDP: v0.rar (52.21 KB, 下载次数: 107) (公网测试需要服务端有独立IP地址,并且有设置路由器端口转发的权限。本机测试直接双击test.bat就可以,client发送的文字可以被server接收到。使用方法:先输入“udp_server 9000”;再输入“udp_client x.x.x.x 9000”)
TCP:http://www.0xaa55.com/thread-392-1-1.html
回复 赞! 1 靠! 0

使用道具 举报

发表于 2014-3-17 06:58:36 | 显示全部楼层
系统自动沙发
回复 赞! 靠!

使用道具 举报

KxIX 该用户已被删除
发表于 2014-3-21 08:38:07 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2014-3-21 14:43:11 | 显示全部楼层
美俪女神 发表于 2014-3-17 02:18
说实话,LZ的文章写得好,例子实在不够清晰。
下面附上我的例子,本机、局域网、公网均测试成功。
UDP:( ...

屌!
回复 赞! 靠!

使用道具 举报

发表于 2014-3-22 15:32:38 | 显示全部楼层

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

使用道具 举报

发表于 2014-3-26 16:27:54 | 显示全部楼层
路过。学习了
回复 赞! 靠!

使用道具 举报

发表于 2014-4-18 18:31:21 | 显示全部楼层
看着这么多的源码,感觉好爽
回复 赞! 靠!

使用道具 举报

发表于 2014-5-3 12:09:20 | 显示全部楼层
.相见恨晚啊!
回复 赞! 靠!

使用道具 举报

发表于 2014-5-6 09:53:00 | 显示全部楼层
相见恨晚                                                                           
回复 赞! 靠!

使用道具 举报

发表于 2014-5-6 22:51:45 | 显示全部楼层
最近刚刚学这个,谢谢作者
回复 赞! 靠!

使用道具 举报

发表于 2014-5-9 14:45:01 | 显示全部楼层
神啊,终于让我找到了!
回复 赞! 靠!

使用道具 举报

发表于 2014-7-18 14:51:03 | 显示全部楼层
Winsock在Windows下的编程教程
回复 赞! 靠!

使用道具 举报

发表于 2014-8-1 17:17:38 | 显示全部楼层
很好的例子。
回复 赞! 靠!

使用道具 举报

发表于 2014-8-6 19:57:37 | 显示全部楼层
漏主的井绳是值得学习的
回复 赞! 靠!

使用道具 举报

卡卡 该用户已被删除
发表于 2015-6-12 16:59:11 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
回复 赞! 靠!

使用道具 举报

发表于 2015-6-13 11:48:06 | 显示全部楼层
外加上各种字符集..也是够蛋疼的.
回复 赞! 靠!

使用道具 举报

发表于 2015-7-14 20:01:46 | 显示全部楼层
啊啊啊啊啊啊啊啊啊啊啊啊啊啊
回复 赞! 靠!

使用道具 举报

发表于 2015-8-25 15:05:55 | 显示全部楼层
不错的教程 复习下
回复 赞! 靠!

使用道具 举报

发表于 2015-9-4 09:45:55 | 显示全部楼层
求学习!!!!!!!!!!!!!!!!!!!
回复

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-11-21 18:12 , Processed in 0.053735 second(s), 34 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

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