(一)网络编程基础之套接字入门

套接字基础

首先,我们来思考下这样一个问题:为什么要使用套接字进行网络编程?

答:Linux环境下使用套接字进行进程之间的通信。套接字接口(socket interface)是一组函数,也是操作系统提供给应用程序的接口。在Unix系统中,套接字和Unix I/O函数结合起来,用来创建网络应用程序。(也就是说,操作系统对外只提供了套接字作为网络通信的接口,假如想进行网络通信,套接字我们用也得用,不用也得用,而且使用套接字来进行网络通信是十分通用的方法)。这里最典型的就是客户端--服务器模型。

因特网客户端和服务器通过在**连接**上发送的接收字节流来通信。从连接一对进程的意义上而言,连接是**点对点**的。从数据可以同时双向流动的角度来说,它是**全双工**的。并且从(除了一些灾难性的引起的失败以外,如:农民伯伯切断了电缆。)由源进程发出的字节流最终被目的进程以它发出的顺序收到它的角度来说,它也是**可靠的**。这个可靠性,主要是有TCP传输来保证的,至于TCP如何保证可靠传输,请参考TCP相关资料。

一个**套接字**是连接的一个端点。每个套接字都有相应的**套接字地址**,是由一个因特网地址(IP地址)和一个16位的整数**端口**组成的,用"地址:端口"来表示。当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为**临时端口**(ephemeral port)。然而,服务器套接字地址中的端口通常是和某个**知名**的端口,是和这个服务相对应的。例如,Web服务器通常使用端口80,而电子邮件服务器使用端口25.在Unix机器上,文件/etc/services包含一张这台机器提供的服务以及它们的知名端口号的综合列表。

一个连接是由它两端的套接字地址唯一确定的。这对套接字地址叫做**套接字对**(socket pair),由下列元组来表示:

(cliaddr:cliport, servaddr:servport)

其中cliaddr是客户端的IP地址,cliport是客户端的端口,servaddr是服务器的IP地址,而servport是服务器的端口。例如,有如下连接套接字对:

(128.2.194.242:51213, 208.216.181.15:80)

在这个示例中,Web客户端的套接字地址是:

128.2.194.242:51213

其中,端口号51213是内核分配的临时的端口号。

Web服务器的套接字地址是:

208.216.181.15:80

其中,端口号80是和Web服务器的通用的端口号。给定了客户端和服务器套接字的地址和端口号。客户端和服务器之间的连接就由下列套接字对唯一确定了:

(128.2.194.242:51213, 208.216.181.15:80)

(也就是说,IP地址,确定了唯一的主机,而端口号,确定了,主机上唯一的进程,这样,就可以使客户端的某一个指定的进程和服务器某一指定的进程进行网络通信)

套接字的地址结构
从Unix内核的角度来看,一个套接字就是通信的一个端点。从Unix程序的角度来看,套接字就是一个有相应描述符(套接字描述符)的打开文件,应用程序可以像操作文件一样操作一个套接字,因此,在进行网络通信的过程中,用户感觉就好像在操作一个文件一样,这也正是Linux将外部设备抽象为一个文件的好处。

准备工作

字节序

每一台主机由于体系结构的不同,所采用的数据存储方式也不相同。在网络环境中,进程之间的通信是要跨越主机的,这时就有了一个字节序不统一的问题。字节序依赖于具体主机的处理器体系结构,同一台主机的进程之间不存在该问题。但是在网络环境汇总变成,程序员不能对通信两端的主机做强制性要求,这样会降低代码的通用性。

为了解决这个问题,网络协议提供一种字节序,当跨越主机的两个进程进行通信时,先将需要传输的数据转换成网络字节序,待接受方接收数据后,再将其转换为本地主机字节序。

因为因特网主机可以有不同的主机字节顺序,TCP/IP为任意整数数据项定义了统一的网络字节顺序(network byte order)(大端字节序)(big-endian),例如IP地址,它放在包头中跨过网络被携带。在IP地址结构中存放的地址总是以(大端法)网络字节顺序存放的,即使主机字节顺序(host byte order)是小端法。Unix提供了下面这样的函数在网络和主机字节顺序间实现转换:

 1 #include <netinet/in.h>
 2
 3 unsigned long int htonl(unsigned long int hostlong);
 4
 5 unsigned short int htons(unsigned short int hostshort);
 6
 7                                     /* 返回:按照网络字节顺序的值 */
 8
 9 unsigned long int ntohl(unsigned long int netlong);
10
11 unsigned short int ntohs(unsigned short int netshort);
12
13                                     /* 返回:按照主机字节顺序的值*/

备注:

其实big endian是指低地址存放最高有效字节(MSB),而little 
endian则是低地址存放最低有效字节(LSB)。用文字说明可能比较抽象,下面用图像加以说明。比如数字0x12345678在两种不同字节序CPU中的存储顺序如下所示:

Big Endian
一个Word中的高位的Byte放在内存中这个Word区域的低地址处

低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 12 | 34 | 56 | 78 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Little Endian
一个Word中的低位的Byte放在内存中这个Word区域的低地址处

低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 78 | 56 | 34 | 12 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

获取IP地址

因特网的套接字地址存放在如下结构中,如下图所示的类型为sockaddr_in的16字节结构中。对于因特网应用,sin_family成员是AF_INET,sin_port成员是一个16位的端口号,而sin_addr成员就是一个32位的IP地址。IP地址和端口号总是以网络字节顺序(大端法)存放的。

 1 ------------------------sockaddr:socketbits.h (included by socket.h), sockaddr_in:netinet/in.h
 2
 3 /* Generic socket address structure (for connect, bind, adn accept) */
 4 struct sockaddr {
 5 unsigned short sa_family;    /* Protocol family */
 6 char    sa_data[14]; /* Address data */
 7 };
 8
 9 /* Internet-style socket address structure */
10 struct sockaddr_in {
11 unsigned short sin_family;    /* Address family (always AF_INET) */
12 unsigned short sin_port;    /* Port number in network byte order */
13 struct in_addr sin_addr;    /* IP address in network byte order */
14 unsigned char sin_zero[8];    /* Pad to sizeof(struct sockaddr) */
15 };
16
17 --------------------sockaddr:socketbits.h(included by socket.h), sockaddr_in:netinet/in.h

##### _in后缀意味着什么?

_in 后缀是互联网(internet)的缩写,而不是输入(input)的缩写。
**connect、bing和accept函数要求一个指向与协议相关的套接字地址结构的指针。套接字接口的设计者面临的问题是,如何定义这些函数,使之能接受各种类型的套接字地址结构。** **现在,我们可以使用通用的void*指针**,那时在C中并不存在这种类型的指针。解决办法是定义套接字函数要求一个指向通用sockaddr结构的指针,然后要求应用程序将与协议特定的结构的指针强制转换成这个通用结构。为了简化代码示例,我们跟随
Stevens的指导,定义下面的类型:
typedef struct sockaddr SA;
然后无论何时需要将sockaddr_in结构强制转换成通用sockaddr结构,我们都使用这个类型。

为什么已经有了socket_addr_in还要设计socketaddr这样的类型,也就是说为什么connect/bind/之类的不直接把参数定义成sockaddr_in或者直接将下面的sockaddr_in定义为sockaddr?反而要在传参或者初始化过程中通过强制类型转换?

答:主要参考上一段中**包括的部分内容** 通俗说,不同的系统或者说不同的网络协议族(网络通信协议),结构是不一样的,这样通过再加一层封装以后,可以是一个统一的结构,也就是,所有结构都是sockaddr.
sockaddr 结构的定义在/usr/include/x86_64-linux-gnu/bits/socket.h中
sockaddr_in 结构定义在/usr/include/netinet/in.h中

参看in.h中,还有其他的协议族,比如下面的sockaddr_in6, 应该也可以调用操作系统的同一套接口,即:connect/bind之类的接口,但是,应该只需强制类型转换为sockaddr即可。

accept函数
服务器通过调用accept函数来等待来自客户端的连接请求:
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr * addr, int *addrlen);
返回:若成功则为非负连接描述符,若出错则为-1。

accpet函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个**已连接描述符**(connected descriptor),这个描述符可被用来利用Unix I/O函数与客户端通信。

监听描述符和已连接描述符之间的区别使很多人感到迷惑。

监听描述符是作为客户端链接请求的一个端点。典型地,它被创建一次,并存在于服务器的整个生命周期。

已连接描述符是客户端和服务器之间已经建立起来了的连接的一个端点。服务器每次接受连接请求时都会创建一次,它只存在于服务器为一个客户端服务的过程中。

第一步。服务器调用accept,等待连接请求到达监听描述符,具体的我们设定为描述符3.(回忆一下,描述符0~2是预留给了标准文件的)
1.服务器阻塞在accpet,等待监听描述符listenfd上的连接请求。
第二步。客户端调用connect函数,发送一个连接请求到listenfd。
2.客户端通过调用和阻塞在connect,创建连接请求。
第三步。accept函数打开了一个新的已连接描述符connfd(我们假设是描述符4),在clientfd和connfd之间建立连接,并且随后返回connfd给应用程序。客户端也从connect返回,在这一点以后,客户端和服务器就可以分别通过读和写clientfd和connfd来回传送数据了。
3.服务器从accept返回connfd。客户端从connect返回。现在在clientfd和connfd之间已经建立起了链接。

备注:为何要有监听描述符和已连接描述符,他们之间有什么区别?
答:你可能很想知道为什么套接字接口要区别监听描述符和已连接描述符。乍一看,这像是不必要的复杂化。然而,区分这两者其实是非常有用而且有必要的,因为它使得我们可以建立并发服务器,它能够同时处理许多客户端连接。例如,每次一个连接请求到达监听描述符时,我们可以派生一个新的进程,它通过已连接描述符与客户端通信。

上面说的这样的服务器,即是简单的echo服务器,一次只能处理一个客户端。这种类型的服务器一次一个地在客户间迭代,成为迭代服务器(iterative server)

而,同时可以处理多个客户端的socke请求的服务器,我们称之为并发服务器(soncurrent server).

为何要有监听描述符和已连接描述符之间的区别?
因为,区分这两者是很有用的,因为它使得我们可以建立并发服务器,它能够同时处理多个客户端连接。例如,每次一个连接请求到达监听描述符时,我们可以派生(fork)一个新的进程,它通过已连接描述符与客户端通信。

----------------------------
参考资料:
可能部分内容有修改。

《深入理解计算机系统》 Page 625, 623, 629。

http://www.cnblogs.com/lidp/archive/2009/12/02/1697485.html

时间: 2024-10-13 11:27:33

(一)网络编程基础之套接字入门的相关文章

Unix网络编程--卷一:套接字联网API 读书笔记

UNIX网络编程--卷一:套接字联网API 本书面对的读者是那些希望自己编写的程序能够使用成为套接字(socket)的API进行彼此通信的人. 目录: 1.简介 2.传输层:TCP.UDP和SCTP 3.套接字编程简介 4.基本TCP套接字编程 5.TCP客户/服务器程序例子 6.I/O复用:select和poll函数 7.套接字选项 8.基本UDP套接字编程 9.基本SCTP套接字编程 10.SCTP客户/服务器程序例子 11.名字与地址转换 12.IPV4与IPV6互操作性 13.守护进程和

Linux网络编程:原始套接字的魔力【上】

基于原始套接字编程 在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发层面,数据的传输特性由TCP或UDP来保证: 也就是说,对于TCP或UDP的程序开发,焦点在Data字段,我们没法直接对TCP或UDP头部字段进行赤裸裸的修改,当然还有IP头.换句话说,我们对它们头部操作的空间非常受限,只能使用它们已经开放给我们的诸如源.目的IP,源.目的端口等等. 今天我们讨论一下原始套接字的程序开发,用它作为入门协议栈的进阶跳板太合适不过了.OK闲话不多说,进入正题. 原始

linux网络编程-(socket套接字编程UDP传输)

今天我们来介绍一下在linux网络环境下使用socket套接字实现两个进程下文件的上传,下载,和退出操作! 在socket套接字编程中,我们当然可以基于TCP的传输协议来进行传输,但是在文件的传输中,如果我们使用TCP传输,会造成传输速度较慢的情况,所以我们在进行文件传输的过程中,最好要使用UDP传输. 在其中,我们需要写两个程序,一个客户端,一个服务端,在一个终端中,先运行服务端,在运行客户端,在服务端和客户端都输入IP地址和端口号,注意服务端和客户端的端口号要相同,然后选择功能,在linux

《网络编程》原始套接字 ---ping程序实现

概述 基于字节流套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)不可以访问传输层协议,只是对应用层的报文进行操作,传输层的数据报格式都是由系统提供的协议栈实现,用户只需要填充相应的应用层报文,由系统完成底层报文首部的填充并发送.原始套接字(SOCK_RAW)可以访问位于基层的传输层协议,原始套接字没有端口号. 原始套接字(SOCK_RAW)是一种不同于 SOCK_STREAM.SOCK_DGRAM 的套接字,它实现于系统核心.原始套接字使进程可以读与写 ICMP.IGMP

UNIX网络编程:socket套接字(TCP与UDP)

套接字简介: 套接字是网络编程中的一种通信机制,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程.应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题.凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行,Linux所提供的功能(如打印服务,ftp等)通常都是通过套接字来进行通信的,套接字的创建和使用与管道是有区别的,

Unix网络编程随手记——套接字接口函数

套接字接口(socket interface)是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用.大多数现代系统上都实现套接字接口,包括所有的Unix变种.Windows和Macintosh. 1.套接字的基本结构 struct sockaddr 这个结构用来存储套接字地址. 数据定义: 1 struct sockaddr 2 { 3 unsigned short sa_family; /* address族, AF_xxx */ 4 char sa_data[14]; /* 14

《网络编程》路由套接字

概述 Unix 系统集成了路由功能,它包含相应的路由数据库可提供的路由信息,用户可以通过命令方式来增加.修改以及删除路由表中的项目,也可以只查看路由表的信息.在创建套接字时,可以通过指定参数 AF_ROUTE 域创建路由套接字,路由套接字可以访问内核中路由子系统的接口信息.路由套接字上支持 3 种类型的操作: 进程通过写到路由套接字向内核发送消息: 进程通过读入路由套接字接收来自内核的消息: 进程调用 sysctl 函数获取路由表或列出所有已配置的接口: 数据链路地址结构 struct sock

【Unix网络编程】chapter3 套接字编程简介

chapter3套接字编程简介3.1 概述 地址转换函数在地址的文本表达和他们存放在套接字地址结构中的二进制值之间进行转换.多数现存的IPv4代码使用inet_addr和inet_ntoa这两个函数,不过这两个新函数inet_pton和inet_ntop同时适用于IPv4和IPv6. 3.2 套接字地址结构 sockaddr_ 3.2.1 IPv4套接字地址结构 IPv4套接字地址结构通常也称为"网际套接字地址结构",它以sockaddr_in命令,定义在<netinet/in.

2017.07.16 Python网络编程之在套接字服务器中使用ThreadingMixIn

直接上代码,之后再解释: # -*- coding: UTF-8 -*-# 或许基于某些原因你不想编写基于进程的应用程序,而更愿意编写多线程应用程序# 和之前的基于ForkingMixIn的套接字服务器一样,使用ThreadingMixIn编写的套接字服务器要遵循相同的回显服务器编程模式# ThreadedServr继承自TCPServer和ThreadingMixIn,客户端连接这个多线程版服务器时,会创建一个新线程# !usr/bin/env python# Python Network P