admin 管理员组

文章数量: 1086866

网络编程之端口与套接字

网络编程之端口与套接字

写给将来的自己看,对于其他人不敢保证可读性。

端口

概念与用途

端口是TCP/IP协议簇中,应用层进程传输层协议实体间的通信接口
应用层进程通过系统调用与某个端口进行绑定,然后就可以通过该端口接收或发送数据。
应用层进程与端口有一一对应的关系,所以可以用端口标识通信的网络应用进程。

端口号

每个端口拥有一个端口号,用于区别不同的端口。
端口与传输层的协议是密不可分的,必须区别是TCP的端口还是UDP的端口。
TCPUDP各自的端口号是相互独立的,如TCP有一个端口号255,UDP也有一个端口号255,两者并不冲突,也没有任何联系。

实现与访问

端口是操作系统可分配的一种资源,是一种抽象的软件机制,包括一些数据结构和I/O缓冲区。
在TCP/IP的实现中,端口操作类似一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问它。

分配机制

全局(静态)分配与本地(动态)分配相结合。
将全部65535个端口号分为保留端口(熟知端口)号和自由端口号。
保留端口范围是0~1023,由一个公认的中央机构根据需要进行统一分配,静态地分配给Internet上著名的众所周知的服务器进程,由于一种服务使用一种应用层协议,也可以说把保留端口分配给了一些应用层协议。

其余端口号,1024~65535,称为自由端口号,采用本地动态的分配方法。
具体来说,应用层进程当需要访问传输层服务时,向操作系统提出申请,操作系统返回一个本地唯一的端口号,进程再通过调用合适的系统调用将自己与该端口绑定,然后通过它进行网络通信。

三元组

网络通信中通信的进程分别是在不同的计算机上,而两台主机还可能位于不同的网络中。
因此在Internet中定位一个应用进程,需要三级寻址:

  1. 指定主机所在的特定网络地址,即网络ID
  2. 主机应有唯一的地址,即主机ID
  3. 每一主机上每一应用进程应有在该主机上了的唯一标识符。
    在TCP/IP中,主机IP地址就是由网络ID和主机ID组成的。
    而应用进程用TCP或UDP的16位端口号标识。
    综上所述,在Internet中,可以用一个三元组标识一个应用层进程:
    应用层进程 = (传输层协议,主机IP地址,传输层端口号)

五元组

在Internet中,一个完整的网间进程通信需要由两个进程组成,两个进程是通信的两个端点,并且只能用同一种传输层协议,因此一个完整的网间通信需要一个五元组标识:
(传输层协议,本地机IP地址,本地机传输层端口,远地机IP地址,远地机传输层端口)

套接字

Socket,是网络中不同主机上应用进程之间进行双向通信的端点的抽象,从效果上来说,一个套接字就是网络上进程通信的一端。

套接字编程接口是应用程序与协议栈软件之间的接口,定义了应用程序与协议栈软件进行交互时可以使用的一组操作。
套接字编程接口给出了应用程序能够调用的一组过程,以及这些过程所需的产生,每个独立的过程完成一个与协议栈软件交互的基本操作。
例如,可能有一个过程用来建立通信连接,另一个过程用来接收数据。

两种实现方式

两种套接字在语义上是相同的,在使用上也具有相似性,所以程序具有可移植性。

一、在操作系统的内核中增加相应的软件

套接字函数时操作系统本身的功能调用,是操作系统内核的一部分。

二、开发操作系统之外的库函数

套接字库的过程是需要链接到应用程序中,并驻留于应用程序地址空间的,当应用程序从套接字库调用过程时,控制转向库程序,它接着调用一个或多个底层操作系统的功能调用,来完成套接字编程接口的功能。
因此,套接字库的库程序对应用程序隐蔽了本机操作系统,而只给出了一个套接字编程接口

UNIX的I/O模式

套接字编程接口最初是作为UNIX操作系统的一个部分发展而来的,被纳入了UNIX操作系统的传统的输入/输出(I/O)概念的范畴。
先来了解UNIX操作系统的I/O模式:

  1. open:用户进程调用open命令,获得对指定文件或设备的使用权,并返回一个描述符(标识文件或设备的短整型数)。
  2. read | write: 多次调用“读”或者“写”命令传输数据。
  3. close: 传输操作完成后,调用close命令,通知操作系统已经完成了对某对象的使用,释放所占用的资源。

套接字与传统I/O的相似之处

  1. 过程类似:沿用了打开——读——写——关闭模式:先创建套接字,然后使用它,最后将它删除。
  2. 方法类似:使用描述符标识套接字。
  3. 使用的过程的名字可以相同,如read,write

不同

  1. 需要建立不同的计算机上的进程间的联系
  2. 需要建立通用机制支持多种协议
  3. 一个使用套接字的应用程序必须说明许多细节,协议簇,远程计算机的地址,该应用进程是客户机还是服务器,服务类型是面向连接还是无连接。所以在创建套接字后,需要再调用其他函数说明套接字的细节。

套接字的三种类型

  1. 数据报套接字。提供无连接、不可靠、独立的数据报传输服务。UDP
  2. 流式套接字。提供双向、有序、无重复、无记录无边界、可靠的数据流传输服务。TCP
  3. 原始式套接字。允许直接访问较低层次的协议(如IP、ICMP),用于检验新的协议的实现。

套接字的使用

套接字由应用层的通信进程创建,并为其服务。操作该套接字的代码是该进程的组成部分。
在生成套接字描述符后,要将套接字与计算机上特定的IP地址和传输层端口号相关联,这个过程称为绑定。一个套接字要使用一个确定的三元组网络地址信息,使它在网络中被唯一地标识。

流式套接字的工作过程

  1. 双方创建并安装套接字
  2. 建立连接:三次握手
  3. 会话期:发送、接收数据。会话的内容是有一定格式的,一来一往的数据交换必须遵守一定顺序,这些都由应用层协议规定
  4. 释放连接

套接字编程接口的系统调用

  1. 创建套接字SOCKET()创建一个套接字并返回一个整型描述符。
int SOCKET( int Protofamily , int Type , int Protocol);

参数Protofamily:协议簇
参数Type:流式或数据报
参数Protocol: 传输层协议(Internet通信域中一般取0,因为Type就已经决定了传输层协议)

  1. 绑定
int BIND(int Sockfd, struct sockaddr * My_addr, int Addrlen);

Sockfd:套接字描述符
My_addr:保存特定的网络地址(IP地址+传输层端口号)
Addrlen:sockaddr的长度
在服务器端,用作监听客户机端连接请求的套接字一定要绑定,因为大多数服务器经常使用熟知端口,并且有时安装多块网卡,具有多个IP地址。
在客户机端一般不必绑定,除非要指定网络地址。

  1. 启动监听
int LISTEN( int Sokfd, int Queuesize);

Queuesize:等待连接队列的最大长度,最大可设为20,一般为5~10
这个调用仅用于服务器端,告诉套接字开始监听客户机的连接请求,并且规定了等待连接队列的最大长度。
操作系统为每个监听套接字各自建立一个用来等待连接的缓冲区队列。

  1. 接受连接请求
int ACCEPT(int Sockfd, struct sockaddr* Addr, int *addrlen);

Addr:出口参数,当此调用执行完毕时,这个变量中放的是所接收的客户机端的网络地址。
addrlen:出口参数,调用时初始设置为Addr结构的长度,执行完毕后客户机端的网络地址长度。

  1. 请求建立连接
int CONNECT(int Sockfd, struct sockaddr* Service_addr, int Addrlen);

Service_addr:服务器端的网络地址信息,实际指出了本调用要连接的目的地。
Addrlen:sockaddr的长度
本调用仅用于客户机端,用来请求连接到服务器。

  1. 读/写套接字
int READ(int sockfd, void * buffer, int len);
int WRITE(int sockfd, void * buffer, int len);
  1. 向套接字发送和从套接字接受
int SEND(int sockfd, char * buffer, int len, int flags);
int RECV(int sockfd, char * buffer, int len, int flags);

recv和send函数提供了和read和write差不多的功能。但是他们提供了第四个参数来控制读写操作.
flags:执行本调用的方式,一般设为0

前面的三个参数和read,write相同,第四个参数能够是0或是以下的组合:
flags意义
MSG_DONTROUTE不查找路由表
MSG_OOB接受或发送带外数据
MSG_PEEK查看数据,并不从系统缓冲区移走数据
MSG_WAITALL等待任何数据
0和read,write一样的操作
  1. 关闭套接字
int CLOSE(int sockfd);

用MFC写SMTP客户端程序

微软基础类库(英语:Microsoft Foundation Classes,简称MFC)是微软公司提供的一个类库(class libraries),以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。
主要参考了《网络编程实用教程》(第2版)叶树华。
SMTP 的全称是“Simple Mail Transfer Protocol”,即简单邮件传输协议。它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。SMTP 服务器就是遵循 SMTP 协议的发送邮件服务器。

  1. 使用MFC AppWizard创建应用程序框架
    工程名是SMTP,应用程序的类型是基于对话框的,对话框的标题是“接收电子邮件客户端程序”,需要Windows Socket的支持,其他部分接受系统的默认设置就可以。向导自动为应用程序创建了两个类。
    应用程序类:CSMTPApp,基类是CWinApp,对应的文件是SMTP.h和SMTP.cpp。
    对话框类:CSMTPDlg,基类是CDialog,对应的文件是SMTPDlg.h和SMTPDlg.cpp。

  2. 为对话框添加控件


    生成的对话框:

  3. 定义控件的成员变量
    用类向导为对话框中的控件对象定义响应的成员变量

  4. 为对话框中的控件对象添加事件响应函数
    用类向导为对话框中的控件对象添加事件响应函数

//用户点击“发送”按钮时,执行此函数
void CSMTPDlg::OnIDOK() 
{// TODO: Add your control notification handler code heresmtpSocket.SetParent(this);UpdateData(TRUE);           //取来用户在对话框中输入的数据smtpSocket.Create();        //创建套接字对象的底层套接字smtpSocket.Connect((LPCSTR)m_Server,25); //连接smtp服务器//	m_Info = "Connected error\r\n";UpdateData(FALSE);		//更新对话框}
  1. 创建一个普通的类CBase64,用来实现Base64编码和解码
    该类向外提供的接口如下:
class CBase64  
{
public:CBase64();virtual ~CBase64();//方法virtual void  Encode(const PBYTE, DWORD);virtual void  Decode(const PBYTE, DWORD);virtual void  Encode(LPCSTR sMessage);virtual void  Decode(LPCSTR sMessage);virtual LPSTR  DecodedMessage() const;virtual LPSTR  EncodedMessage() const;virtual LONG  DecodedMessageSize() const;virtual LONG  EncodedMessageSize() const;
};

6.创建从CAsyncSocket类继承的派生类
为了能够捕获并响应socket事件,应创建用户自己的套接字类,可利用类向导添加。
Class Tyep选择MFC Class,类名为mySock,基类是CAsyncSocket类,创建后对应的文件是mysock.h和mysock.cpp。再利用类向导添加OnConnect、OnClose、OnReceive3个事件处理函数。

并为它添加一般的成员函数和变量:

public:CString lastMsg;CString error;
void SetParent(CSMTPDlg* pDlg);
private:void AnalyzeMsg();     CSMTPDlg* m_pDlg;
CBase64 coder;STATE state;

7.手工添加包含语句、事件函数和成员函数的代码
要点如下:
(1) 运用Windows的消息驱动机制
(2) 通过状态转换来控制会话命令的发布顺序

//表示smtp会话状态的枚举类型
typedef enum {START=0,HELO,AUTH,USER,PASS,MAIL,RCPT,DATA,QUIT} STATE;
//根据smtp会话所处的状态做出不同的处理	switch(state){case START:if(statusCode=="220") {s.Format("HELO %s%c%c",m_pDlg->m_Name,13,10);//CRLFSend((LPCSTR)s,s.GetLength()); //发送EHLO命令state=HELO;}break;case HELO:if(statusCode == "250"){s.Format("AUTH LOGIN%c%c",13,10); Send((LPCSTR)s,s.GetLength()); //发送AUTH LOGIN命令state=AUTH;}break;case AUTH:if(statusCode == "334"){coder.Encode(m_pDlg->m_User);s.Format(_T("%s%c%c"),coder.EncodedMessage(),13,10);Send((LPCSTR)s,s.GetLength()); //发送用户名state=USER; }break;case USER:if(statusCode == "334"){coder.Encode(m_pDlg->m_Pass);s.Format(_T("%s%c%c"),coder.EncodedMessage(),13,10);Send((LPCSTR)s,s.GetLength()); //发送用户名state=PASS; }break;case PASS:if(statusCode == "235"){s.Format("MAIL FROM:<%s>%c%c",m_pDlg->m_Addr,13,10); Send((LPCSTR)s,s.GetLength()); //发送MAIL FROM命令state=MAIL;}break;case MAIL:if(statusCode == "250"){s.Format("RCPT TO:<%s>%c%c",m_pDlg->m_Receiver,13,10); Send((LPCSTR)s,s.GetLength()); //发送RCPT TO命令state=RCPT;}break;case RCPT:if(statusCode == "250"){s.Format("DATA%c%c",13,10); Send((LPCSTR)s,s.GetLength()); //发送MAIL FROM命令state=DATA;}break;case DATA:if(statusCode == "354"){m_pDlg->GetHeader();state=QUIT;}break;case QUIT:if(statusCode == "250"){s.Format("QUIT%c%c",13,10); Send((LPCSTR)s,s.GetLength()); //发送QUIT命令state=DATA;}break;}

8.分阶段编译执行,进行测试

本文标签: 网络编程之端口与套接字