计算机网络知识
两台主机能否通信需要满足三个条件:
1.两台主机需要有自己的IP 地址,相当于有自己的身份。
2.两台主机要进行通信需要遵循约定的规则,这种规则称之为协议,需要采用相同的协议。
3.为了标记计算机运行的每个网络通信程序,需要为它们分配一个端口号。这样指定的IP 计算机,将由指定的端口号上等待数据的网络应用程序接收数据。
TCP/IP 模型
TCP/IP 模型 包括四个层次:
应用层:对应于OSI 七层参考模型中会话层、表示层和应用层
传输层:对应于OSI 七层参考模型中 传输层
网络层:对应于OSI 七层参考模型中 网络层
网络接口层:对应于OSI 七层参考模型中的 数据链路层和 物理层
端口
端口是一种抽象的软件结构(包括一些数据结构和I/O 缓冲区) 。应用程序通过调用与某端口建立连接(binding) 后,传输层传给该端口的数据就被相应的进程所接收,相应的进程发给传输层的数据都通过该端口输出。端口号使用16 位数字表示,范围 0-65535,1024 以下的端口号保留给预定义的服务,我们编写的网络应用程序,指定1024 以上的端口号。
套接字
套接字存在于通信区域中,通信区域也叫地址族,主要用于将通过套接字通信的进程的公有特性综合在一起。Widonws Sockets 只支持一个通信区域:网际域(AF_INET)。这个域被使用网际协议族通信进程中使用。
套接字的类型
流式套接字(SOCK_STREAM)
提供面向连接、可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收。柳市套接字实际上是基于TCP 协议实现的。
数据报式套接字(SOCK_DGRAM)
提供无连接服务,数据包以独立包形式发送,不提供无差错保证,数据可能丢失或重复,并且接收顺序混乱,数据报式套接字实际上是基于UDP 协议实现的。
原始套接字(SOCK_RAW)
基于TCP(面向连接)的socket 编程
服务器端程序流程:
1、创建套接字
2、将套接字绑定到一个本地地址和端口上(bind)
3.将套接字设为监听模式,准备接收客户请求(listen)
4.等待客户请求到来:当请求到来后,接收连接请求,返回一个新的应用于此次连接的套接字(accept)
5.用返回的套接字和客户端进行通信(send/recv)
6.返回,等待另外一个客户请求。
7.关闭套接字。
客户端流程如下:
1.创建套接字(socket)
2.向服务器发出连接请求(connect)。
3.和服务器端进行通信(send/recv)
4.关闭套接字
基于UDP(面向无连接)的socket 编程
接收端程序编写(服务器端)
1、创建套接字
2、将套接字绑定到一个本地地址和端口上(bind)
3、等待接收数据(recvfrom)
4、关闭套接字
发送数据端(客户端)
1、创建套接字
2、向服务器发送数据(sendto)
3、关闭套接字
windows socket 网络编程相关函数:
1.WSAStartup
int WSAStartup (
WORD wVersionRequested,
LPWSADATA lpWSAData
);
功能:初始化 WinSock DLL。本函数调用成功后才可以调用其他 Winsock 函数。
参数:
WORD wVersionRequested 加载套接字库的版本号。 高位字节为副版本号, 低位字节
为主版本号。一般可以用 MAKEWORD 宏来为这个参数赋值(MAKEWORD 宏的使用见文
章最后的附录) 。
LPWSADATA lpWSAData 出参 指向一个 WSADATA 结构体。这个结构体中包含了
系统加载套接字库的相关信息(WSADATA 结构体具体见本文 WSADATA 部分) 。
返回值:
成功返回 0,失败返回非零。常见的失败代码如下(注意这些失败代码不是通过
WSAGetLastError 获得的,因为本函数没有调用成功,根本无法分配资源。所以这些代码是
直接返回的) :
WSASYSNOTREADY 10091 底层的网络子系统无法使用
WSAVERNOTSUPPORTED 请求的版本系统不支持
WSAEINPROGRESS 一个阻塞的 Socket 正在调用
WSAEPROCLIM 任务数量已经到达了 Socket 所能够支持的最大数量
WSAEFAULT 参数 lpWSAData 的指针时无效指针
由于 Windows 将 socket 的实现放到动态库中,在用 Winsock 的 API 编程时,需要加载动态库。如果是加载 2.2 版本的套接字库,要使用 winsock2.h 头文件。示例代码如下
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
上面的代码是调用 Winsock2.2 版本。版本 1.1 是 Winsock.h 和 wsock32.lib。
WSAData 结构体
struct WSAData {
WORD wVersion; //winsock dll 期望调用者使用的版本
WORD wHighVersion; //winsock dll 支持的最高版本号
char szDescription[WSADESCRIPTION_LEN+1]; //描述字符串
char szSystemStatus[WSASYSSTATUS_LEN+1]; //系统配置信息
unsigned short iMaxSockets; //winsoc1.1 中单个进程能够创建最大 socket 数,2.0 后
//的版本没有用
unsigned short iMaxUdpDg; //2.0 后的版本没有用
char FAR * lpVendorInfo; //2.0 后的版本没有用
};
1.2 .WSACleanup
int WSACleanup (void)
功能:释放套接字库。
返回值:成功返回 0,失败返回 SOCKET_ERROR(-1)。
每一次成功的调用了 WSAStartup, 都应该有对应的 WSAClearup 调用, 以便于 Winsock DLL 释放启动时分配的资源。WSAStartup 和 WSAClearup 可以多次调用,为了记录程序的调用次数,WinSock 使用一个内部计数器,初始值为 0,调用一次 SAStartup 加一,调用一次 WSAClearup 减一。当减到 0 时,WSAClearup 进行清除操作,它取消进程中任何线程中正在进行的阻塞或异步调用,而且不发送通知或者向事件对象发送信号。WSAClearup 也
取消正在进行的重叠操作,并且不设置事件对象或调用完成函数。
2.创建
int socket(int domain, int type, int protocol);
参数说明:
domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。
返回值:
如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。
3.绑定
函数原型:
int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len);
参数说明:
socket:是一个套接字描述符。
address:是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。
address_len:确定address缓冲区的长度。
返回值:
如果函数执行成功,返回值为0,否则为SOCKET_ERROR。
4.接收
函数原型:
int recv(SOCKET socket, char FAR* buf, int len, int flags);
参数说明:
socket:一个标识已连接套接口的描述字。
buf:用于接收数据的缓冲区。
len:缓冲区长度。
flags:指定调用方式。取值:MSG_PEEK 查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;MSG_OOB 处理带外数据。
返回值:
若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
函数原型:
ssize_t recvfrom(int sockfd, void buf, int len, unsigned int flags, struct socketaddr* from, socket_t* fromlen);
参数说明:
sockfd:标识一个已连接套接口的描述字。
buf:接收数据缓冲区。
len:缓冲区长度。
flags:调用操作方式。是以下一个或者多个标志的组合体,可通过or操作连在一起:
(1)MSG_DONTWAIT:操作不会被阻塞;
(2)MSG_ERRQUEUE: 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。错误以sock_extended_err结构形态被使用。
(3)MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
(4)MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长, 只对packet套接字有效。
(5)MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
(6)MSG_EOR:指示记录的结束,返回的数据完成一个记录。
(7)MSG_TRUNC:指明数据报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。
(8)MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。
(9)MSG_OOB:指示接收到out-of-band数据(即需要优先处理的数据)。
(10)MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据。
from:(可选)指针,指向装有源地址的缓冲区。
fromlen:(可选)指针,指向from缓冲区长度值。
5.发送
函数原型:
int sendto( SOCKET s, const char FAR* buf, int size, int flags, const struct sockaddr FAR* to, int tolen);
参数说明:
s:套接字
buf:待发送数据的缓冲区
size:缓冲区长度
flags:调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
addr:(可选)指针,指向目的套接字的地址
tolen:addr所指地址的长度
返回值:
如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR。
函数原型:
int accept( int fd, struct socketaddr* addr, socklen_t* len);
参数说明:
fd:套接字描述符。
addr:返回连接着的地址
len:接收返回地址的缓冲区长度
返回值:
成功返回客户端的文件描述符,失败返回-1。
向一个已连接的套接口发送数据。
1.Server 端在 stdafx.h 中增加语句:
include <AfxSock.h> //加载socket 头文件
在UDP_Sean_server.cpp 中 BOOL CUDP_Sean_serverApp::InitInstance() 中初始化
if(!AfxSocketInit())//加载套接字库,进行版本协商
{
AfxMessageBox("加载套接字失败!");
return FALSE;
}
2.初始化套接字、创建接收线程
void CUDP_Sean_serverDlg::OnBnClickedBtnStart()
{
// TODO: 在此添加控件通知处理程序代码
InitSocket(); //初始化套接字
RECVPARAM *pRecvParam=new RECVPARAM;//用来传递参数的结构体
pRecvParam->hwnd=m_hWnd;
pRecvParam->sock=m_socket;
HANDLE hThread=CreateThread(NULL,0,RecvProc/*线程函数*/,
(LPVOID)pRecvParam,0,NULL);//创建线程
CloseHandle(hThread);//关闭线程句柄
}
struct RECVPARAM
{
SOCKET sock;
HWND hwnd;
};
3.初始化套接字
BOOL CUDP_Sean_serverDlg::InitSocket(void)
{
m_socket = socket(AF_INET,SOCK_DGRAM,0);
if(INVALID_SOCKET == m_socket)
{
AfxMessageBox(_T("套接字创建失败"));
return FALSE;
}
SOCKADDR_IN addrSock;
addrSock.sin_family=AF_INET;
addrSock.sin_port=htons(6000);
addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
//addrSock.sin_addr.S_un.S_addr=htonl();
//INADDR_ANY表示接收任意IP地址
//绑定
int retval;//用来接收bind返回值
retval=bind(m_socket,(SOCKADDR*)&addrSock,
sizeof(SOCKADDR));
if(SOCKET_ERROR==retval)
{//绑定失败
closesocket(m_socket);
AfxMessageBox(_T("绑定失败"));
return FALSE;
}
return TRUE;
}
4.创建线程用于显示接收的数据
DWORD WINAPI CUDP_Sean_serverDlg::RecvProc(LPVOID lpParameter)
{
SOCKET sock=((RECVPARAM*)lpParameter)->sock;
HWND hwnd=((RECVPARAM*)lpParameter)->hwnd;
//SOCKADDR_IN addrFrom;
int len=sizeof(SOCKADDR);
char recvBuf[200]={0};
char tempBuf[400]={0};
int retval;
while(TRUE)
{
retval=recvfrom(sock,recvBuf,200,NULL,
(SOCKADDR*)&addrFrom,&len);
if(SOCKET_ERROR==retval)
break;
sprintf(tempBuf,"%s said:%s",
inet_ntoa(addrFrom.sin_addr),recvBuf);
::PostMessage(hwnd,WM_RECVDATA,0,(LPARAM)tempBuf);
//通过发送消息方式把数据传给对话框,
//传递一个自定义消息WM_RECVDATA
//将数据通过lParam进行传递。
}
return 0;
}
5.自定义消息
#define WM_RECVDATA WM_USER+1
afx_msg LRESULT OnRecvData(WPARAM wParam, LPARAM lParam);
ON_MESSAGE(WM_RECVDATA,OnRecvData)
LRESULT CUDP_Sean_serverDlg::OnRecvData(WPARAM wParam, LPARAM lParam)
{
CString str=(char*)lParam;
CString strTemp;
GetDlgItemText(IDC_EDIT_RECV,strTemp);
// str+="\r\n";//换行
str=strTemp+_T("\r\n")+str;
SetDlgItemText(IDC_EDIT_RECV,str);
return true;
}
6.向客户端发送消息
void CUDP_Sean_serverDlg::OnBnClickedBtnSend()
{
// TODO: 在此添加控件通知处理程序代码
DWORD dwIP;
int result = SOCKET_ERROR;
//2.定义地址结构体变量
// SOCKADDR_IN addrTo;
// addrTo.sin_addr.S_un.S_addr=inet_addr("10.0.2.10");
// addrTo.sin_family=AF_INET;
// addrTo.sin_port=htons(6000);
//3.得到发送编辑框口要发送的数据
CString strSend;
GetDlgItemText(IDC_EDIT_SEND,strSend);
//4.发送数据
result = sendto(m_socket,strSend,strSend.GetLength()+1,0,
(SOCKADDR*)&addrFrom,sizeof(SOCKADDR));
if(result == SOCKET_ERROR)
{
MessageBox("Send Error!","Send",MB_ICONERROR | MB_OK);
}
else
{
MessageBox("Send OK!","Send",MB_OK);
}
SetDlgItemText(IDC_EDIT_SEND,"");
//把发送框设为空白
}
Client 端编程
1.client 套接字初始化
BOOL CUDP_Sean_serverDlg::InitSocket(void)
{
CString ipStr;
CString ipPort;
int lenght;
char sendBuf[100] ={"\0"};
m_socket = socket(AF_INET,SOCK_DGRAM,0);
if(INVALID_SOCKET == m_socket)
{
AfxMessageBox(_T("套接字创建失败"));
return FALSE;
}
GetDlgItemText(IDC_EDIT_IP,ipStr);
GetDlgItemText(IDC_EDIT_PORT,ipPort);
int port = atoi(ipPort);
//SOCKADDR_IN addrSock;
addrSock.sin_family=AF_INET;
addrSock.sin_port=htons(port);
addrSock.sin_addr.S_un.S_addr=inet_addr(ipStr);
//addrSock.sin_addr.S_un.S_addr=htonl();
//INADDR_ANY表示接收任意IP地址
//绑定
lenght = sizeof(SOCKADDR);
sendto(m_socket,sendBuf,sizeof(sendBuf)+1,0,(SOCKADDR*)&addrSock,lenght);
SetDlgItemText(IDC_EDIT_IP,"");
SetDlgItemText(IDC_EDIT_PORT,"");
return TRUE;
}
2.初始化套接字并且创建线程
void CUDP_Sean_serverDlg::OnBnClickedBtnStart()
{
// TODO: 在此添加控件通知处理程序代码
InitSocket(); //初始化套接字
RECVPARAM *pRecvParam=new RECVPARAM;//用来传递参数的结构体
pRecvParam->hwnd=m_hWnd;
pRecvParam->sock=m_socket;
HANDLE hThread=CreateThread(NULL,0,RecvProc/*线程函数*/,
(LPVOID)pRecvParam,0,NULL);//创建线程
CloseHandle(hThread);//关闭线程句柄
}
3.线程函数接收数据
DWORD WINAPI CUDP_Sean_serverDlg::RecvProc(LPVOID lpParameter)
{
SOCKET sock=((RECVPARAM*)lpParameter)->sock;
HWND hwnd=((RECVPARAM*)lpParameter)->hwnd;
//SOCKADDR_IN addrFrom;
int len=sizeof(SOCKADDR);
char recvBuf[200]={0};
char tempBuf[400]={0};
int retval;
while(TRUE)
{
retval=recvfrom(sock,recvBuf,200,NULL,
(SOCKADDR*)&addrSock,&len);
if(SOCKET_ERROR==retval)
break;
sprintf(tempBuf,"%s said:%s",
inet_ntoa(addrSock.sin_addr),recvBuf);
::PostMessage(hwnd,WM_RECVDATA,0,(LPARAM)tempBuf);
//通过发送消息方式把数据传给对话框,
//传递一个自定义消息WM_RECVDATA
//将数据通过lParam进行传递。
}
return 0;
}
4.自定义消息函数
LRESULT CUDP_Sean_serverDlg::OnRecvData(WPARAM wParam, LPARAM lParam)
{
CString str=(char*)lParam;
CString strTemp;
GetDlgItemText(IDC_EDIT_RECV,strTemp);
// str+="\r\n";//换行
str=strTemp+_T("\r\n")+str;
SetDlgItemText(IDC_EDIT_RECV,str);
return true;
}
5.发送消息函数
void CUDP_Sean_serverDlg::OnBnClickedBtnSend()
{
// TODO: 在此添加控件通知处理程序代码
//DWORD dwIP;
int result = SOCKET_ERROR;
//2.定义地址结构体变量
// SOCKADDR_IN addrTo;
// addrTo.sin_addr.S_un.S_addr=inet_addr("10.0.2.10");
// addrTo.sin_family=AF_INET;
// addrTo.sin_port=htons(6000);
//3.得到发送编辑框口要发送的数据
CString strSend;
GetDlgItemText(IDC_EDIT_SEND,strSend);
//4.发送数据
result = sendto(m_socket,strSend,strSend.GetLength()+1,0,
(SOCKADDR*)&addrSock,sizeof(SOCKADDR));
if(result == SOCKET_ERROR)
{
MessageBox("Send Error!","Send",MB_ICONERROR | MB_OK);
}
else
{
MessageBox("Send OK!","Send",MB_OK);
}
SetDlgItemText(IDC_EDIT_SEND,"");
//把发送框设为空白
}