socket 编程基础

这是我准备的我们班第三次交流学习的内容,由我讲的。主要是关于socket编程的。这些内容只是最基本的、最简单的网络编程方面的内容而且只涉及到了字节流套接字,而且并发服务器编程的内容几乎未涉及,只能是让我们对于网络编程有了一个初步的认识。我对于网络编程并没什么深入的研究,这是我最近看《深入理解计算机系统》这本书,看到了网络编程这一部分的内容,想分享给同学们,所以花了不到两周的时间看了许多关于网络编程的内容,但是下面的文章感觉眼界还是比较窄,涉及的内容也比较单一。结构安排是按照我在学习过程中碰到的一些问题调整的,主要的参考就是《深入理解计算机系统》和《UNIX网络编程》。

 

BSD SocketAPI

一.      套接字接口的起源

80年代初,美国国防部高级研究计划署ARPA让California大学在UNIX操作系统下实现TCP/IP协议,Berkley提出了为UNIX操作系统开发的网络通信接口Socket,它是建立在传输层协议(主要是TCP和IP)上的一种套接字规范,因此人们也将Socket接口称为Berkeley Socket。Socket概念最早出现于1983年的4.2BSD版本中,它的主要目的是提供一个统一的访问网络和进程间通信协议的接口。

除了Berkeley Socket外还有Windows Sockets (Winsock) 、Java Sockets、Python sockets、Perl sockets

 Linux 所支持的 BSD 套接字类型

BSD 套接字类型

 描述

流(stream)

这种套接字提供了可靠的双向顺序数据流,可保证数据不会在传输过程中丢失、破坏或重复出现。流套接字通过 INET 地址族的TCP 协议实现。

数据报(datagram)

 这种套接字也提供双向的数据传输,但是并不对数据的传输提供担保,也就是说,数据可能会以错误的顺序传递,甚至丢失或破坏。这种类型的套接字通过 INET 地址族的UDP 协议实现。

原始(raw)

 利用这种类型的套接字,进程可以直接访问底层协议(因此称为原始)。例如,可在某个以太网设备上打开原始套接字,然后获取原始的 IP 数据传输信息。

可靠发送的消息

 和数据报套接字类似,但保证数据被正确传输到目的端。

顺序数据包

 和流套接字类似,但数据包大小是固定的。

数据包(packet)

 这并不是标准的 BSD 套接字类型,它是Linux 专有的 BSD 套接字扩展,可允许进程直接在设备级访问数据包。

 

二.      客户端—服务器模型(Client&Server)

几乎每个网络都是基于客户端—服务器模型的,采用这个模型,一个应用是由一个服务器进程和多个客户端进程组成的。服务器管理某种资源并通过操作向客户端提供服务。例如一个浏览器就属于客户端程序,而web服务器、ftp服务器则属于服务器,管理很多磁盘文件。

      客户端—服务器模型的基本操作是事务,一个客户端—服务器模型由四步组成:

1.     当一个客户端需要服务时,它向一个服务器发送一个请求,发起一个事务。

2.     服务器收到请求后,解释它并以适当的方式操作它的资源。

3.     服务器给客户端发送一个响应,并等待下一个请求。

4.     客户端受到响应并处理它。


客户端和服务器是进程,不是主机。

 

三.      基本概念

1.     端口

端口号是一个16bit的整数,它用来区分不同的进程和不同的服务。客户端通常对它所使用的端口号并不关心,只需保证该端口号在本机上是唯一的就可以了,客户端口号又称作临时端口号(即存在时间很短暂)。通常服务器的端口号是固定的一些知名端口号,如http是80,telnet是23等。

2.     IP

IP地址不在多说,都早已不再陌生,需要说明的一点是它是一个unsigned int类型的数据,通常以一种称为点分十进制表示法来表示。它和端口号共同组成了套接字对(socket pair),套接字对唯一的标识了一个网络上的每个TCP连接。它是一个四元组:本地IP、本地TCP端口号、外地IP、外地TCP端口号。

3.     套接字(socket)

套接字应该是最重要的一个概念,那到底什么是套接字呢?我们通常成一个标识一个端点的两个值IP/port成为一个套接字。从Unix内核的角度来看,一个套接字就是通信的一个端点。从Unix程序的角度来看一个套接字就是一个有相应描述符的打开文件。

4.     套接字描述符

在socket中会用到套接字描述符,个人觉得这是一个理解socket编程重要概念,为了便于理解,先看一下文件描述符,在linux中所有设备都是被抽象成文件的,对设备的读写都可以看成对文件的读写。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时。内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。

而套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里

5.     网络字节顺序(大端法小端法)

在计算机中存储数据有两种方式成为大端法(big-endian)和小端法(little-endian)。大端法是将高位字节存储在起始地址,即地位地址,而小端法正好反过来,网际协议所用字节序则为大端法,在套接字地址结构中某些字段必须按照网络字节顺序维护,比如说IP地址。

                                                           大端法与小端法的区别


四.      几个基本结构

IP地址结构:

structin_addr{

unsignedint s_addr;  /*Network byte order(big-endian)*/

}

DNS主机结构条目:

structhostent{

char  *h_name;    /*Officialdomain name of host*/

char  *h_aliases; /*Null-terminatedarray of domain names*/

int   h_addrtype;     /*Hostaddress type (AF_INET)*/

int      h_length;   /*lengthof an address, in bytes*/

char **h_addr_list; 

/*Null-terminatedarray of in_addr structs */

};

套接字地址结构:

通用的结构体

structsockaddr {

unsignedshort sa_family; /* address family, AF_xxx */

charsa_data[14];        /* 14 bytes of protocoladdress */

 };

sa_family是地址家族,一般都是“AF_xxx”的形式,通常大多用的是都是AF_INET。 
sa_data是14字节协议地址。

编程时常用的

structsockaddr_in {

shortint sin_family;        /*Address family */

unsignedshort int sin_port; /* Port number */

structin_addr sin_addr;      /* Internet address */

unsignedchar sin_zero[8];/* Same size as struct sockaddr */

};

sin_family指代协议族,在socket编程中只能是AF_INET 
sin_port存储端口号(使用网络字节顺序) 
sin_addr存储IP地址,使用in_addr这个数据结构 
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。 
s_addr按照网络字节顺序存储IP地址  
sockaddr_in和sockaddr是并列的结构,指向sockaddr_in的结构体的指针也可以指向 sockadd的结构体,并代替它。在使用时我们常常强制类型转换将sockaddr_in转换为sockaddr的通用结构。

五.      常用的几个函数

下面我们说几个在网络编程中几乎总是用到的函数,掌握这些函数对于分析一个客户端/服务器程序是很有帮助的哟。

1.  gethostbyname() 和 gethostbyaddr()

gethostbyname() 和 gethostbyaddr()函数是用来解析主机名和地址的。可能会使用DNS服务或者本地主机上的其他解析机制(例如查询/etc/hosts)。返回一个指向struct hostent的指针,这个结构体描述一个IP主机。函数使用如下参数:

§  name 指定主机名。例如 www.google.com

§  addr 指向 structin_addr的指针,包含主机的地址。

§  len 给出 addr的长度,以字节为单位。

§  type 指定地址族类型 (比如 AF_INET)。

出错返回NULL指针,可以通过检查 h_errno 来确定是临时错误还是未知主机。正确则返回一个有效的 struct hostent *。

这些函数并不是伯克利套接字严格的组成部分。这些函数可能是过时了,新函数是 getaddrinfo() and getnameinfo(), 这些新函数是基于addrinfo数据结构。

函数原型:

struct hostent *gethostbyname(const char *name);

struct hostent *gethostbyaddr(const void *addr, int len, int type);

 

2. htonl()和 ntohl()

htonl函数将整数由主机字节顺序转换为网络字节顺序,ntohl函数将整数由网络字节顺序转换为主机字节序。

函数原型:

unsigned long inthtonl(unsigned long int hostlong);

unsigned long int ntohl(unsignedlong int netlong);

3. inet_aton和inet_ntoa

inet_aton函数将一个点分十进制串转换为一个网络字节序的IP地址,inet_ntoa将一个网络字节序的IP地址转换为点分十进制字符串。

 

函数原型:

int inet_aton(const char *cp, struct in_addr *inp);

char *inet_ntoa(struct in_adddr in);

 

4. 字节操纵函数:

void *bzero(void *dest, size_t nbytes);

void *bcopy(const void *src, void *dest, size_t nbytes);

void bcmp(const void *ptrl, const void *ptr2, size_t nbytes);

bzero把目标字节串中指定数目的字节置为0.我们常使用该函数把一个套接字地址结构初始化为0。bcopy将指定数目的字节从源字串移动到目的字串。bcmpy比较任意的两个字节串,相同返回0,否则返回非0.这三个函数来自于Berkeley函数,下面是在ANSI C中对应的三个函数:

void *memset(void *dest, size_t nbytes);

void *memcpy(const void *src, void *dest, size_t nbytes);

int memcmp(const void *ptrl, const void *ptr2, size_t nbytes);

 

5.  I/O函数

ssize_t recv(int sockfd, void *buff, size_t nbytes,int flags);

不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。

客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。

该函数的第一个参数指定发送端套接字描述符;第二个参数指明一个存放应用程序要发送数据的缓冲区;第三个参数指明实际要发送的数据的字节数;

第四个参数一般置0;返回值是字符串长度。

ssize_t send(int sockfd, const void *buff, size_tnbytes, int flags);

不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。

该函数的第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;第三个参数指明buf的长度;第四个参数一般置0;返回值是字符串长度。

六.      API



基本TCP客户/服务器程序套接字函数


以上两张图展示了套接字函数和客户端与服务器端建立连接的过程,让我们有了一个初步的印象,下面就看看具体函数的实现吧,我已经迫不及待了:


socket()

int socket(int family, int type, intprotocol);

为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数,socket() 为通信创造一个端点并返回一个文件描述符。就好像我们想与朋友通电话,得先安个电话不是,或者有个手机也可以。 socket()返回的描述符仅是部分打开的,还不能用于读写。如何完成打开套接字的工作取决于我们是客户端还是服务器。

socket() 由三个参数:

§  family,确定协议族。例如:

§  PF_INET 是IPv4 或者

§  PF_INET6 是 IPv6.

§  PF_UNIX 是本地(用一个文件).

§  type,是下面中的一个:

§  SOCK_STREAM (可靠的面向连接的服务或者 Stream Sockets)

§  SOCK_DGRAM (数据包服务或者 Datagram Sockets)

§  SOCK_SEQPACKET (可靠的有序的分组服务),或者

§  SOCK_RAW (网络层的原始协议)。

§  protocol 确定实际使用的运输层。最常见的是 IPPROTO_TCPIPPROTO_SCTPIPPROTO_UDPIPPROTO_DCCP。这些协议是在<netinet/in.h>中定义的。如果 domain和 type已经确定,“0” 可以用来选择一个默认的协议。

如果出错返回-1,否则返回一个代表文件描述符的整数。

connect()

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

connect()系统调用为一个套接字设置连接,客户端通过调用connect()函数来建立和服务器的链接。就想我们拿起了电话开始拨号的过程,而serv_addr就是我们要播的电话号码。其中sockfdsocket函数的返回的套接字描述符。connect()函数试图与套接字地址为serv_addr的服务器建立一个因特网链接。connect函数会阻塞,一直等到链接成功建立或是发生错误。

注意:某些类型的套接字是无连接的,大多数是UDP协议。对于这些套接字,连接时这样的:默认发送和接收数据的主机由给定的地址确定,可以使用send()和recv()。 返回-1表示出错,0表示成功。

 

bind()

int bind(intsockfd, const structsockaddr *my_addr, socklen_taddrlen);

bind() 给套接字分配一个地址。当使用 socket()创造一个套接字时, 只是给定了协议族,并没有分配地址。在套接字能够接受来自其他主机的连接前,必须用bind()给它绑定一个地址。 bind可以指定IP地址或端口号,也可以都指定也可以都不指定 。bind() 由三个参数:

§  sockfd,代表socket的文件描述符。

§  my_addr,指向 sockaddr 结构体的指针,代表要绑定的地址 。

§  addrlen,是sockaddr结构体的大小。

bind()返回0表示成功,错误返回-1。

listen()

int listen(intsockfd, int backlog);

listen函数仅由TCP服务器调用,它做两件事:

1. 当一个socket函数创建一个套接字时它被假设为一个主动套接字,也就是说它是一个将调用connect函数发起链接的客户套接字。Listen函数将其转化为一个被动的监听套接字

2.     第二个参数规定了内核应该为相应套接字排队的最大连接个数。这个在看了connect函数之后在理解就容易了。

就好像你朋友一直在等在电话旁边,他一直在监听(listen)。

listen()需要两个参数:

§  sockfd,一个有效的套接字描述符。

§  backlog,一个整数,表示一次能够等待的最大连接数目。操作系统通常会对这个值设置上限。

一旦连接被接受,返回0表示成功,错误返回-1。

accept()

int accept(int listenfd, structsockaddr *cliaddr, socklen_t *addrlen);

accept函数由TCP服务器调用,用于从已完成的连接队列头返回下一个已完成的连接。如果accept函数成功,那么其返回值是一个由内核生成的全新的描述符代表与所返回客户的TCP连接。表示已经与客户端建立了连接,可以进行通信了。accept()为每个连接创立新的套接字并从监听队列中移除这个连接。

这个过程就像你的朋友拿起电话听筒,就可以开始通话了,

它使用如下参数:

§  listenfd,监听的套接字描述符

§  cliaddr,指向sockaddr结构体的指针,客户机地址信息。

§  addrlen,指向 socklen_t的指针,确定客户机地址结构体的大小。

返回新的套接字描述符,出错返回-1。进一步的通信必须通过这个套接字。

注意:datagram套接字不要求用accept()处理,因为接收方可能用监听套接字立即处理这个请求。

 

前面这些内容只是最基本的、最简单的网络编程方面的内容而且只涉及到了字节流套接字,而且并发服务器编程的内容几乎未涉及。以上只是使我们对于网络编程有了一个初步的认识,想进一步研究的话强烈推荐W.RichardStevens的《UNIX网络编程》,绝对的经典呀!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值