注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

jasonyang9的博客

随便写写

 
 
 

日志

 
 

(怀旧系列)VC程序设计(孙鑫老师)听课笔记:14 网络编程  

2013-02-16 19:43:36|  分类: programming |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
(怀旧系列)VC程序设计(孙鑫老师)听课笔记:14 网络编程 - jasonyang9 - jasonyang9的博客
========
网络编程
========

计算机网络是相互连接的独立自主的计算机的集合,最简单的网络形式由两台计算机组成。

                      协议                          协议
      计算机A-------------------------网络-------------------------计算机B
192.168.0.118 + 端口号                                       192.168.0.10 + 端口号


关于IP地址:
* IP网络中每台主机都必须有一个惟一的IP地址(至少一个);
* IP地址是一个逻辑地址;
* 英特网上的IP地址具有全球唯一性;
* 32位,4个字节,常用点分十进制的格式表示(例如:192.168.0.16)。

关于协议:
* 为进行网络中的数据交换(通信)而建立的规则、标准或约定(协议=语义+语法+规则);
* 不同层具有各自不同的协议。

关于网络的状况:
* 多种通信媒介(有线、无线);
* 不同种类的设备(通用、专用);
* 不同的操作系统(Unix、Windows);
* 不同的应用环境(固定、移动);
* 不同业务种类(分时、交互、实时);
* 宝贵的投资和积累(有形、无形);
* 用户业务的延续性(不允许出现大的跌宕起伏)。
相互交织,形成了非常复杂的系统应用环境。

关于网络异质性问题的解决:
* 网络体系结构就是使这些不同媒介连接起来的不同设备和网络系统在不同的应用环境下实现互操作性,并满足各种业务需求的一种粘合剂,营造了一种“生存空间”(任何厂商的任何产品以及任何技术只要遵守这个空间的行为规则,就能在其中生存并发展)。
* 网络体系结构解决异质性问题采用的是分层方法(把复杂的网络互联问题划分为若干个较小的、单一的问题,在不同层上予以解决)。就像在编程时把问题分解为很多小的模块来解决一样。

关于ISO/OSI七层参考模型:
* OSI(Open System Interconnection)参考模型将网络的不同功能划分为7层。

应用层 处理网络应用(为用户的应用程序提供网络通信的服务)
表示层 数据表示(处理被传送数据的表示问题(信息的语法、语义),如有必要,使用一种通用的数据表示格式,在各种表示格式之间进行转换,和数据的加解密,压缩解压缩等)
会话层 主机间通信(在两个相互通信的应用进程之间建立、组织、协调相互之间的通信)
传输层 端到端的连接(为源端主机到目的端主机提供可靠的数据传输服务,隔离网络的上下层协议,使网络应用和下层(具体实现)无关)
网络层 寻址和最短路径(提供IP寻址和路由(网络上数据到达目的地有多条线路))
数据链路层 介质访问(接入)(加强物理层的传输功能,建立一条无差错的传输线路)
物理层 二进制传输(确定在通信信道上如何传输比特流)


OSI只是按照功能划分的一种抽象的参考模型。

* 通信实体的对等层之间不允许直接通信(也不可能直接通讯?)。
* 各层之间是严格单向依赖。
* 上层使用下层提供的服务(Service User)。
* 下层向上层提供服务(Service Provider)。

对等通信示例:

对交谈内容的共识
(中国教师)“你好” ←- - - - - - -→ “Hallo”(德国教师)
  用英语对话
(翻译)“Hello” ←- - - - - - -→ “Hello”(翻译)
 使用传真通信
(秘书)传真 ←- - - - - - -→ 传真(秘书)
物理通信线路-------------------------物理通信线路


对等层通信的实质:
* 对等层实体之间虚拟通信;
* 下层向上层提供服务,实际通信在最底层完成。

OSI各层所使用的协议:
* 应用层:远程登录协议Telnet、文件传输协议FTP、超文本传输协议HTTP、域名服务DNS、简单邮件传输协议SMTP、邮局协议POP3等;
* 传输层:传输控制协议TCP(面向连接的、可靠的传输协议)、用户数据报协议UDP(无连接的、不可靠的传输协议);
* 网络层:网际协议IP、Internet互联网控制报文协议ICMP、Internet组管理协议IGMP;

数据封装:
* 一台计算机要发送数据到另一台计算机,数据首先必须打包,打包的过程称为封装;
* 封装就是在数据前面加上特定的协议头部;

数据
协议头 + 数据


* OSI参考模型中,对等层协议之间交换的信息单元统称为协议数据单元(PDU,Protocol Data Unit);
* OSI参考模型中每一层都要依靠下一层提供的服务;
* 为了提供服务,下层把上层的PDU作为本层的数据进行封装,然后加入本层的头部(一些层还会加上尾部,如数据链路层),头部中含有完成数据传输所需的控制信息;
* 这样,数据自上而下递交的过程实际上就是不断封装的过程。到达目的地后,自下而上递交的过程就是不断拆封的过程。由此可知,在物理线路上传输的数据,其外面实际上被包封了多层“信封”;
* 但是,某一层只能识别由对等层封装的“信封”,而对于被封装在“信封”内部的数据仅仅是拆封后将其提交给上层,本层不作任何处理(通常来说)。

TCP/IP模型:
* TCP/IP起源于美国国防部高级研究规划署(DARPA)的一项研究计划(实现若干台主机的相互通信);
* 现在TCP/IP已成为Internet上通信的工业标准;
* TCP/IP模型包括4个层次:应用层、传输层、网络层和网络接口层。

TCP/IP和OSI参考模型的对应关系:

OSI参考模型 TCP/IP模型

应用层 应用层
表示层
会话层
传输层 传输层
网络层 网络层
数据链路层
物理层 网络接口层


端口:
* 按照OSI七层模型的描述,传输层提供进程(应用程序)通信的能力。为了标识通信实体中进行通信的进程(应用程序),TCP/IP协议提出了协议端口(Protocol Port,简称端口)的概念;
* 端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序通过系统调用和某端口建立连接(Binding)后,传输层给该端口的数据都被相应的进程所接收,相应进程发给传输层的数据都通过该端口输出;
* 端口用一个整数型标识符来表示,即端口号。端口号和协议相关,TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也是相互独立的;
* 端口使用一个16位的数字来表示,范围是0~65535,1024以下的端口号保留给预定义的服务(例如:http使用80端口)。

关于套接字(Socket)的引入:
* 为了能够方便的开发网络应用软件,由美国伯克利大学在Unix上推出了一种应用程序访问通信协议的操作系统调用Socket(套接字)。Socket的出现,使程序员可以很方便的访问TCP/IP,从而开发各种网络应用的程序;
* 随着Unix的应用推广,套接字在编写网络软件中得到了极大的普及。之后,套接字又被引进了Windows等操作系统中,成为开发网络应用程序的非常有效和快捷的工具;
* 套接字存在于通信区域中,通信区域也称为地址族,是一个抽象的概念,主要用于将通过套接字通信的进程的共有特性综合在一起。套接字通常只和同一区域的套接字交换数据(也有可能跨区域通信,但需要执行某种转换进程)。Windows Sockets只支持一个通信区域:网际域(AF_INET),这个域被使用网际协议簇通信的进程使用。

关于网络字节顺序:
* 不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低位先存),有的机器在起始地址存放高位字节(高位先存)。基于Intel的CPU(即常用的PC机)采用的是低位先存。为保证数据的正确性,在网络协议中需要指定网络字节顺序,TCP/IP协议使用16位整数和32位整数的高位先存格式。

客户机/服务器模式:
* 在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机/服务器模式(Client/Server),即客户向服务器提出请求,服务器接收到请求后,提供相应的服务;
* 客户机/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程建立联系,为二者的数据交换提供同步,这就是基于客户机/服务器模式的TCP/IP(服务器端等待客户端的连接请求)。
* 客户机/服务器模式在操作过程中采取的是主动请求的方式。
首先服务器方要先启动,并根据请求提供相应的服务:
(1)打开一个通信通道并告知本地主机,它愿意在某一地址和端口上接收客户请求(监听某个IP和端口);
(2)等待客户请求到达该端口;
(3)接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一个新的进程(或线程)来处理这个客户请求。新进程(或线程)处理此客户请求,(此进行或线程)并不需要对其他请求作出应答。服务完成后,关闭此新进程和客户的通信链路,并终止;
(4)返回第二步,等待另一客户请求;
(5)关闭服务器。
客户方:
(1)打开一个通信信道,并连接到服务器所在主机的特定端口;
(2)向服务器发服务请求报文,等待并接收应答,继续提出请求;
(3)请求结束后关闭通信通道并终止。

Windows Sockets的实现:
* Windows Sockets是Microsoft Windows的网络程序设计接口,从Berkeley Sockets扩展而来,以动态链接库的形式提供。Windows Sockets在继承了Berkeley Sockets主要特征的基础上,进行了重要扩充。主要是提供了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。
* Windows Sockets 1.1和Berkeley Sockets都是基于TCP/IP协议的,Windows Sockets 2从Windows Sockets 1.1发展而来,和协议无关,并向下兼容,可以使用任何底层传输协议提供的通信能力来为上层应用程序完成网络数据通讯,而不关心底层网络链路的通讯情况,真正实现了底层网络通讯对应用程序的透明。

套接字的类型:
* 流式套接字(SOCK_STREAM),提供面向连接、可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收(基于TCP)。
* 数据报式套接字(SOCK_DGRAM),提供无连接服务。数据包以独立包形式发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱(基于UDP)。
* 原始套接字(SOCK_RAW)。

基于TCP(面向连接)的Socket编程:

服务器程序:
1、创建套接字(socket);
2、将套接字绑定到一个本地地址和端口上(bind);
3、将套接字设为监听模式,准备接收客户请求(listen);
4、等待客户请求到来,当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept);
5、用返回的套接字和客户端进行通信(send/recv);
6、返回,等待另一个客户请求;
7、关闭套接字。

客户端程序:
1、创建套接字(socket);
2、向服务器发出连接请求(connect);
3、和服务器端进行通信(send/recv);
4、关闭套接字。

基于UDP(面向无连接)的Socket编程:

服务器端(接收端)程序:
1、创建套接字(socket);
2、将套接字绑定到一个本地地址和端口上(bind);
3、等待接收数据(recvfrom);
4、关闭套接字。

客户端(发送端)程序:
1、创建套接字(socket);
2、向服务器发送数据(sendto);
3、关闭套接字。

套接字表示了通信的端点,利用套接字和利用电话机通信类似,IP地址相当于总机号码,端口号相当于分机号码。

新建一个Win32 Console Application(TcpSrv),增加C++源文件。

相关函数的说明:

这里要用到WSAStartup函数,加载套接字库和进行套接字库的版本协商。
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
* wVersionRequested参数用于指定准备加载的Winsock库的版本。高位字节指定所需要的Winsock库的副版本,低位字节是主版本,可用MAKEWORD(x, y)(其中x是高位字节,y是低位字节)方便地获得wVersionRequested正确的值。
* lpWSAData参数是指向WSADATA结构的指针,WSAStartup用其加载的库版本有关的信息填写在这个结构中(作为返回值)。
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;


WSAStartup把第一个字段wVersion设成准备使用的Winsock版本,wHighVersion参数容纳的是现有的Winsock库的最高版本。在这两个字段中,高位字节代表的是Winsock副版本,低位字节代表的是Winsock主版本。
szDescription和szSystemStatus这两个字段由特定的Winsock实施方案设定,实际上没有用。
不要使用iMaxSockets和iMaxUdpDg,他们是设定同时最多可打开多少套接字和数据报的最大长度,但要知道数据报的最大长度应该通过WSAEnumProtocols来查询协议信息。同时最多可打开套接字的数目是不固定的,很大程度上和可用物理内存的多少有关。
lpVendorInfo字段是为Winsock实施方案有关的指定厂商信息预留的,任何一个Win32平台上都没有使用这个字段。
如果Winsock.dll或底层网络子系统没有被正确初始化或没有被找到,WSAStartup函数将放回WSASYSNOTREADY。
这个函数允许应用程序协商使用某版本的WinSock规范,如果请求的版本等于或高于DLL所支持的最低版本,WSAData的wVersion成员中将包含应用程序应该使用的版本(是DLL所支持的最高版本和请求版本中较小的那个)。反之,如果请求的版本低于DLL所支持的最低版本,WSAStartup将返回WSAVERNOTSUPPORTED。(MSDN中有完整的表格,用以说明DLL版本和请求版本和最终结果的关系)
对于每一个WSAStartup的成功调用(即成功加载WinSock.dll后),在最后都对应一个WSACleanUp调用,以便释放为该应用程序分配的资源。

SOCKET socket(int af, int type, int protocol);
* 该函数接收3个参数,第一个参数af指定地址族,对于TCP/IP协议的套接字,只能是AF_INET(也可写成PF_INET)。第二个参数指定Socket类型,对于1.1版本的Socket,只支持两种类型的套接字,SOCK_STREAM指定产生流式套接字,SOCK_DGRAM产生数据报套接字。第三个参数是与特定的地址族相关的协议,如果指定为0,那么它就会根据地址格式和套接字类别自动选择一个合适的协议(推荐用这种方法)。
* 如果这个函数调用成功,将返回一个新的SOCKET数据类型的套接字描述符。如果调用失败,这个函数会返回一个INVALID_SOCKET,错误信息可以通过WSAGetLastError函数获得。

int bind(SOCKET s, const struct sockaddr FAR *name, int namelen);
* 这个函数接收3个参数。第一个参数s指定要绑定的套接字,第二个参数指定了该套接字的本地地址信息(一个指向sockaddr结构的指针变量),由于该地址结构是为所有地址家族准备的,这个结构可能(通常是)随所使用的网络协议不同而不同,所以要用第三个参数指定该地址结构的长度。
sockaddr结构定义如下:

struct sockaddr {
u_short sa_family;
char sa_data[14];
};

* sockaddr的第一个字段sa_family指定该地址家族(这里必须设为AF_INET)。sa_data仅仅表示要求一块内存分配区,起到占位符的作用,该区域中指定和协议相关的具体地址信息。由于实际要求的只是内存区,所以对于不同的协议家族用不同的结构来替换sockaddr。除了sa_family外,sock_addr是按网络字节顺序表示的。在TCP/IP中,可以用sockaddr_in结构替换sockaddr,方便填写地址信息。
sockaddr_in的定义如下:

struct sockaddr_in {
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};


其中,sin_family表示地址族,对于IP地址,sin_family总是AF_INET。
sin_port指定的是将要分配给套接字的端口(网络字节顺序)。
sin_addr是套接字的主机IP地址(网络字节顺序,一个结构体in_addr类型)。

struct in_addr {
union {
struct { u_char s_b1, s_b2, s_b3, s_b4; } S_un_b;
struct { u_short s_w1, s_w2; } S_un_w;
u_long S_addr;
} S_un;
};

sin_zero只是一个填充数,以使sockaddr_in结构和sockaddr结构的长度一样。
如果这个函数调用成功,将返回0。如果调用失败,返回一个SOCKET_ERROR,错误信息可以通过WSAGetLastError获得。
将IP地址指定为INADDR_ANY,将允许套接字向任何分配给本地计算机的IP地址发送或接收数据。多数情况下,每台计算机只有一个IP地址,但有些主机可能会有多个网卡,每个网卡都可以有自己的IP地址,用INADDR_ANY可以简化应用程序的编写。将地址指定为INADDR_ANY允许一个独立应用接受发自多个接口(网卡)的回应。
如果想让套接字使用多个IP中的一个地址,就必须指定实际地址,即调用inet_addr()函数,这个函数需要一个字符串作为参数,该字符串指定了以点分十进制格式表示的IP地址(如:192.168.0.16),而且该函数会返回一个适合分配给S_addr的u_long类型的数值(参见in_addr结构体)。
inet_ntoa()函数完成相反的转换,接受一个in_addr结构体类型的参数并返回一个以点分十进制格式表示的IP地址字符串。

#include <Winsock2.h> // 需要包括Winsock2.h
#include <stdio.h> // 标准输入输出

void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1); // 请求1.1版本的Winsock(注意前一个是副版本,后一个是主版本)

err = WSAStartup(wVersionRequested, &wsaData); // 调用WSAStartup,第二个是返回值,要加上取地址符

if (err != 0) // 返回值不等于0说明无法找打一个可用的WinSock.dll
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 || // 判断返回的wVersion低字节和高字节内容是否为请求的版本号
HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup(); // 如果版本不符,直接清理并返回
return;
}

SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
SOCKADDR_IN addrSrv; // 定义地址结构体变量addrSrv
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // sockaddr_in结构体成员中除了sin_family之外都是网络字节序,需要用htonl(转换u_long类型变量从主机字节序到网络字节序)或htons(转换u_short类型变量从主机字节序到网络字节序)转换后赋值。虽然INADDR_ANY宏定义为0,不作转换也可以,但还是应该转换,表明这里使用的是网络字节序
addrSrv.sin_family = AF_INET; // sin_family只能用AF_INET
addrSrv.sin_port = htons(6000); // 端口号为6000(只能用大于1024的端口号)

bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); // 绑定套接字和地址(第2个参数需要做强制类型转换)

listen(sockSrv, 5); // listen使套接字进入监听状态,第2个参数是等待连接队列的最大长度(如果长度为5,则表示只有前5个连接请求被放入队列,第6个和之后的请求被拒绝,应用程序会依次为前5个请求服务。注意这并不是并发处理的请求数量)

SOCKADDR_IN addrClient; // 定义地址结构体变量addrClient用于接收客户端的IP地址
int len = sizeof(SOCKADDR); // 定义一个变量用于获取客户端地址的长度(并初始化为地址结构体的长度,这里必须作初始化,因为紧接着调用的accept函数的第3个参数必须要有一个初始值(为结构体长度)并返回客户端地址的长度,否则调用函数失败(MFC文档有误))

while (1) // 不断等待客户端的连接到来
{
SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len); // 调用accept接收客户端的连接,返回一个代表此连接的套接字
char sendBuf[100]; // 定义字符数组,存放要发送给客户端的数据
sprintf(sendBuf, // 格式化字符数组
"Welcome %s to http://www.sunxin.org", // 为一个字符串
inet_ntoa(addrClient.sin_addr)); // 将客户端的IP地址加入字符串(inet_ntoa转换in_addr结构体类型的地址为点分十进制字符串)
send(sockConn, sendBuf, strlen(sendBuf) + 1, 0); // 用send函数对sockConn套接字发送字符串,多发送一个字节(让客户端可以用\0结尾),flags设为0。注意不要对处于监听状态的sockSrv发送。
char recvBuf[100]; // 定义字符数组,用于接收客户端的数据
recv(sockConn, recvBuf, 100, 0); // 用recv函数接收sockConn套接字中客户端的数据,接收100个字节,flags设为0
printf("%s\n", recvBuf); // 打印出来
closesocket(sockConn); // 关闭套接字,释放资源
}
// 如果程序要正常退出,这里应该加上closesocket(sockSrv)和WSACleanup()才算释放了资源
}


还需要在Project Settings中的Link选项卡中的Object library modules的最后加上ws2_32.lib(要空格开之前的内容)才能编译通过。这是利用动态链接库的方法。

在工作区中增加一个新的工程TcpClient,编写客户端的代码。
(为了能够区分当前的工程和切换,可以将Build工具栏打开,即可看到工程的下拉列表)

#include <Winsock2.h> // 需要包括Winsock2.h
#include <stdio.h> // 标准输入输出

void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1); // 请求1.1版本的Winsock(注意前一个是副版本,后一个是主版本)

err = WSAStartup(wVersionRequested, &wsaData); // 调用WSAStartup,第二个是返回值,要加上取地址符

if (err != 0) // 返回值不等于0说明无法找打一个可用的WinSock.dll
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 || // 判断返回的wVersion低字节和高字节内容是否为请求的版本号
HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup(); // 如果版本不符,直接清理并返回
return;
}

SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字

// 客户端程序不需要绑定套接字和地址

SOCKADDR_IN addrSrv; // 定义服务器的地址变量
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 指定服务器的IP地址(由于这里的客户端程序和服务器端都在一台主机上运行,那么就可以指定这个127.0.0.1回环地址代表本机),用inet_addr函数转换点分十进制IP地址到u_long类型
addrSrv.sin_family = AF_INET; // sin_family总是AF_INET
addrSrv.sin_port = htons(6000); // 端口和服务器监听端口一致

connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); // 调用connect函数向服务器发出连接请求
char recvBuf[100]; // 定义字符数组用于接收数据
recv(sockClient, recvBuf, 100, 0); // 接收服务器端的数据
printf("%s\n", recvBuf); // 打印出来
send(sockClient, "This is zhangsan", strlen("This is zhangsan") + 1, 0); // 发送数据给服务器端

closesocket(sockClient); // 关闭套接字,释放资源
WSACleanup(); // 终止对套接字库的使用
}


客户端工程同样需要动态链接ws2_32.dll,方法和服务器端工程一致。

编译链接后,先运行服务器端程序,再运行客户端程序,客户端上可以看到“Welcome 127.0.0.1 to http://www.sunxin.org”,服务器端上可以看到“This is zhangsan”。客户端自动退出后,服务器端仍在运行,再运行一次客户端就能够再次看到传送的数据被显示出来。

以上是基于TCP(流式套接字(SOCK_STREAM))的程序编写。基于UDP(数据报式套接字(SOCK_DGRAM))的程序相对简单。

新建一个工作区,新建一个Win32 Console Application(UdpSrv)工程。
在Project Settings中的Link选项卡中的Object library modules的最后加上ws2_32.lib(要空格开之前的内容)

#include <Winsock2.h> // 需要包括Winsock2.h
#include <stdio.h> // 标准输入输出

void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1); // 请求1.1版本的Winsock(注意前一个是副版本,后一个是主版本)

err = WSAStartup(wVersionRequested, &wsaData); // 调用WSAStartup,第二个是返回值,要加上取地址符

if (err != 0) // 返回值不等于0说明无法找打一个可用的WinSock.dll
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 || // 判断返回的wVersion低字节和高字节内容是否为请求的版本号
HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup(); // 如果版本不符,直接清理并返回
return;
}

// 在使用套接字的网络程序中以上代码是一样的,可以直接复制粘贴

SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字,注意第2个参数为SOCK_DGRAM
SOCKADDR_IN addrSrv; // 定义地址结构体变量addrSrv
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // INADDR_ANY允许套接字向任何分配给本地计算机的IP地址发送或接收数据(注意是网络字节序)
addrSrv.sin_family = AF_INET; // sin_family总是AF_INET
addrSrv.sin_port = htons(6000); // 端口为6000

bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); // 绑定套接字和地址

SOCKADDR_IN addrClient; // 定义地址结构体变量,用来接收发送方的地址信息
int len = sizeof(SOCKADDR); // 定义一个变量用于获取客户端地址的长度(并初始化为地址结构体的长度,这里必须作初始化,因为紧接着调用的recvfrom函数的第6个参数必须要有一个初始值(为结构体长度)并返回客户端地址的长度)
char recvBuf[100]; // 定义字符数组,用于接收数据

recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len); // 接收数据
printf("%s\n", recvBuf); // 打印输出

closesocket(sockSrv); // 关闭套接字,释放资源
WSACleanup(); // 终止对套接字的使用
}


新建一个工程到工作区,UpdClient,增加C++源文件。
在Project Settings中的Link选项卡中的Object library modules的最后加上ws2_32.lib(要空格开之前的内容)

#include <Winsock2.h> // 需要包括Winsock2.h
#include <stdio.h> // 标准输入输出

void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1); // 请求1.1版本的Winsock(注意前一个是副版本,后一个是主版本)

err = WSAStartup(wVersionRequested, &wsaData); // 调用WSAStartup,第二个是返回值,要加上取地址符

if (err != 0) // 返回值不等于0说明无法找打一个可用的WinSock.dll
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 || // 判断返回的wVersion低字节和高字节内容是否为请求的版本号
HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup(); // 如果版本不符,直接清理并返回
return;
}

// 在使用套接字的网络程序中以上代码是一样的,可以直接复制粘贴

SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
SOCKADDR_IN addrSrv; // 定义地址结构体变量addrSrv
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 指定服务器的IP地址(由于这里的客户端程序和服务器端都在一台主机上运行,那么就可以指定这个127.0.0.1回环地址代表本机),用inet_addr函数转换点分十进制IP地址到u_long类型
addrSrv.sin_family = AF_INET; // sin_family总是AF_INET
addrSrv.sin_port = htons(6000); // 端口和服务器端口一致

sendto(sockClient, "Hello", strlen("Hello") + 1, 0, (SOCKADDR*)&addSrv, sizeof(SOCKADDR)); // 发送数据

closesocket(sockSrv); // 关闭套接字,释放资源
WSACleanup(); // 终止对套接字的使用
}


编译链接后,先启动服务器端程序,再启动客户端程序,随即客户端发送数据后终止,服务器端收到数据“Hello”后输出并终止。

一个简单的基于字符界面的聊天程序(一般来说,聊天程序都是基于UPD协议的)。
新建一个工程,Win32 Console Application,NetSrv。
在Project Settings中的Link选项卡中的Object library modules的最后加上ws2_32.lib(要空格开之前的内容)

#include <Winsock2.h> // 需要包括Winsock2.h
#include <stdio.h> // 标准输入输出

void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1); // 请求1.1版本的Winsock(注意前一个是副版本,后一个是主版本)

err = WSAStartup(wVersionRequested, &wsaData); // 调用WSAStartup,第二个是返回值,要加上取地址符

if (err != 0) // 返回值不等于0说明无法找打一个可用的WinSock.dll
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 || // 判断返回的wVersion低字节和高字节内容是否为请求的版本号
HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup(); // 如果版本不符,直接清理并返回
return;
}

// 在使用套接字的网络程序中以上代码是一样的,可以直接复制粘贴

SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字

SOCKADDR_IN addrSrv; // 定义服务器的地址变量
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 指定服务器的IP地址(由于这里的客户端程序和服务器端都在一台主机上运行,那么就可以指定这个127.0.0.1回环地址代表本机),用inet_addr函数转换点分十进制IP地址到u_long类型
addrSrv.sin_family = AF_INET; // sin_family总是AF_INET
addrSrv.sin_port = htons(6000); // 端口为6000

bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); // 绑定套接字和地址

char recvBuf[100]; // 定义接收数据的字符数组
char sendBuf[100]; // 定义发送数据的字符数组
char tempBuf[200]; // 定义存放中间数据的字符数组

SOCKADDR_IN addrClient; // 定义地址结构体变量,用来接收发送方的地址信息
int len = sizeof(SOCKADDR); // 定义一个变量用于获取客户端地址的长度(并初始化为地址结构体的长度)

while (1) // 循环
{
recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len); // 接收数据
if ('q' == recvBuf[0]) // 判断收到的第一个字符是否为‘q’,如果是则退出
{
sendto(sockSrv, "q", strlen("q") + 1, 0, (SOCKADDR*)&addrClient, len); // 给对方也发送一个‘q’(对方的地址就是addrClient,其长度也有了,就是len)(这里的套接字就像一个管道,既可以从sockSrv取出数据,也可以发送数据)
printf("Chat end!\n"); // 输出提示语句
break; // 退出循环
}
sprintf(tempBuf, "%s say : %s", inet_ntoa(addClient.sin_addr), recvBuf); // 格式化接收到的数据(第一个字符串是转换为点分十进制的对方的IP地址,第二个字符串是对方发来的数据)
printf("%s\n", tempBuf); // 输出

printf("Please input data:\n"); // 提示用户输入要发送给对方的数据
gets(sendBuf); // 从stdin获得用户的输入(gets可以直接获取回车前的字符串)
sendto(sockSrv, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrClient, len); // 将数据发送给对方
}

closesocket(sockSrv); // 关闭套接字,释放资源
WSACleanup(); // 终止对套接字的使用
}


然后是客户端程序,增加新工程,Win32 Console Application,NetClient。
在Project Settings中的Link选项卡中的Object library modules的最后加上ws2_32.lib(要空格开之前的内容)

#include <Winsock2.h> // 需要包括Winsock2.h
#include <stdio.h> // 标准输入输出

void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1); // 请求1.1版本的Winsock(注意前一个是副版本,后一个是主版本)

err = WSAStartup(wVersionRequested, &wsaData); // 调用WSAStartup,第二个是返回值,要加上取地址符

if (err != 0) // 返回值不等于0说明无法找打一个可用的WinSock.dll
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 || // 判断返回的wVersion低字节和高字节内容是否为请求的版本号
HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup(); // 如果版本不符,直接清理并返回
return;
}

// 在使用套接字的网络程序中以上代码是一样的,可以直接复制粘贴

SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字,注意第2个参数为SOCK_DGRAM

SOCKADDR_IN addrSrv; // 定义地址结构体变量addrSrv
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 指定服务器的IP地址(由于这里的客户端程序和服务器端都在一台主机上运行,那么就可以指定这个127.0.0.1回环地址代表本机),用inet_addr函数转换点分十进制IP地址到u_long类型
addrSrv.sin_family = AF_INET; // sin_family总是AF_INET
addrSrv.sin_port = htons(6000); // 端口和服务器端口一致

char recvBuf[100]; // 定义接收数据的字符数组
char sendBuf[100]; // 定义发送数据的字符数组
char tempBuf[200]; // 定义存放中间数据的字符数组

int len = sizeof(SOCKADDR); // 定义一个变量用于获取地址的长度(并初始化为地址结构体的长度)

while (1) // 循环
{
printf("Please input data:\n"); // 提示用户输入要发送给对方的数据
gets(sendBuf); // 从stdin获得用户的输入(gets可以直接获取回车前的字符串)
sendto(sockClient, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrSrv, len); // 将数据发送给对方

recvfrom(sockClient, recvBuf, 100, 0, (SOCKADDR*)&addrSrv, &len); // 接收数据
if ('q' == recvBuf[0]) // 判断收到的第一个字符是否为‘q’,如果是则退出
{
sendto(sockClient, "q", strlen("q") + 1, 0, (SOCKADDR*)&addrSrv, len); // 给对方也发送一个‘q’(对方的地址就是addrSrv,其长度也有了,就是len)(这里的套接字就像一个管道,既可以从sockClient取出数据,也可以发送数据)
printf("Chat end!\n"); // 输出提示语句
break; // 退出循环
}
sprintf(tempBuf, "%s say : %s", inet_ntoa(addrSrv.sin_addr), recvBuf); // 格式化接收到的数据(第一个字符串是转换为点分十进制的对方的IP地址,第二个字符串是对方发来的数据)
printf("%s\n", tempBuf); // 输出
}

closesocket(sockSrv); // 关闭套接字,释放资源
WSACleanup(); // 终止对套接字的使用
}


编译链接后,先启动服务器端程序,再启动客户端程序,在客户端中输入内容并回车,服务器端就接收到并显示出来,然后从服务器端输入内容并回车,客户端就接收到并显示出来,在客户端或服务器端输入‘q’能使双方都打印出“Char end!”并结束运行(由于程序的逻辑设定,服务器端和客户端只能依次输入数据发送给对方)。

  评论这张
 
阅读(616)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017