【网络编程技术】基本套接字编程TCP

套接字基础

套接字类型

套接字存在于特定的通信协议(地址族)中,只有类属于同一地址族的套接字才能建立通信,套接字支持多种通信协议:

AF_LOCAL:Unix 系统本地通信

AF_INET:IP版本4

AF_INET6:IP版本6

Linux支持多种套接字类型,套接字类型:是指创建套接字的应用程序所希望的通信服务类型。

SOCKET_STREAM:双向可靠数据流,流式套接字,对应TCP

SOCKET_DGRAM:双向不可靠数据报,数据包套接字,对应UDP

SOCKET_RAW:是低于传输层的低级协议或物理网络直接访问,可以访问内部网络接口。原始套接字,例如接收和发送ICMP报。

套接字地址结构(IPv4)

struct sockaddr_in{

  unsigned short int sin_len;  /* IPv4地址长度 */

  short int sin_family; /* 地址类型 */

  unsigned short int sin_port; /* 存储端口号 */

  struct in_addr sin_addr;  /*存储IP地址 */

  unsigned char sin_zero[8];  /* 空字节 */

};


sin_family指代协议族,在TCP套接字编程中只能是AF_INET;

sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。

sin_port存储端口号(使用网络字节顺序),数据类型是一个16位的无符号整数类型;

sin_addr存储IP地址,IP地址使用in_addr这个数据结构:

  struct in_addr{  unsigned long s_addr;  };

这个数据结构是由于历史原因保留下来,主要用作与以前的格式兼容。这里的s_addr按照网络字节顺序存储IP地址。



设置地址信息的实例(IPv4)

struct sockaddr_in mysock; /*设置sockaddr_in的结构体变量mysock */

mysock.sin_family=AF_INET; /*地址族*/

mysock.sin_port=htons(3490); /*short,NBO*/

mysock.sin_addr.s_addr=inet_addr(“192.168.1.221”); /*设置地址为192.168.1.221*/

bzero(&(mysock.sin_zero),8); /*设置sin_zero为8位保留字节*/

注意:如果mysock.sin_addr.s_addr=INADDR_ANY,则不指定IP地址(用于server程序)。



字节排序函数

网络中存在多种类型的机器,这些不同类型的机器表示数据的字节顺序是不同的。网络协议中的数据采用统一的网络字节顺序,因为只有采用统一的字节顺序,才能在不同类型的硬件设备之间正确的发送和接收数据。广域网规定的网络字节顺序采用大端字节顺序方式。

系统提供4个函数来进行字节顺序转换:

#include “netinet/in.h”

unsigned short int htons(unsigned short int hostshort);

unsigned long int htonl(unsigned long int hostlong);

unsigned short int ntons(unsigned short int netshort);

unsigned long int ntonl(unsigned long int netlong);

h:主机  n:网络  s:短整数  l:长整数

其中。前两个函数将主机字节顺序转换成网络字节顺序;后两个函数将网络字节顺序转换成主机字节顺序。

在使用这些函数时,我们不关心主机或网络顺序的真实值到底是大端还是小端,只需要调用适当的函数来对给定值(函数的整型参数)进行主机字节顺序和网络字节顺序的转换,它们的返回值就是经过转换以后的结果。

字节操纵函数

系统提供两组函数来处理多字节数据,一组函数是以b(byte)开头,和BSD系统兼容的函数;另一组是以mem开头,ANSI C所提供的函数。

#include

void bzero(void *dest, size_t nbytes);

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

int  bcmp(const void *src, void *dest, size_t nbytes); /*返回0则相同,非0不相同*/

                                            上述三个函数源自BSD

void *memset(void *dest, int c, size_t len);

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

int    memcmp(const void *ptr1, const void *ptr2, size_t nbytes)

                                           上述三个函数属于ANSI C



memcpy函数等同于bcopy,差别是bcopy可以处理源src和目标dest相重叠的情况,而memcpy则对这种情况没有定义。

memset函数将参数s指定的内存区域的前n个字节设置为参数c的内容;

bcmp比较任意两个内存区域,即s1指定的内存区域与s2指定的内存区域的前n个字节,若相同则返回值为0,否则返回值为非0

bzero函数将目标中指定数目的字节置为0,这个函数经常用来把套接字地址结构初始化为0,如:

bzero(&servaddr,sizeof(servaddr));

bcopy将指定数目的字节从源src移动到目标dest指定的内存区域;



地址转换函数

地址转换函数负责在ASCII字符串和网络字节顺序的二进制值之间进行地址转换。

inet_aton,inet_addr和inet_ntoa函数

#include

int inet_aton(const char *strptr,struct in_addr *addrptr);

in_addr_t inet_addr(const char *strptr);

inet_aton函数将strptr所指向的字符串转换成32位的网络字节序二进制值,并存储在指针addrptr指向的in_addr结构体中,若成功,返回1。

inet_addr函数,其转换结果作为返回值返回32位二进制网络字节序地址,若转换错,则返回INADDR_NONE。

inet_addr进行相同的转换,但不进行有效性验证,当IP地址是255.255.255.255时,会认为这是个无效的IP地址,但对于目前大部分的路由器上,这个IP都是有效的。

inet_aton函数将tcp所指的字符串(点分十进制数串,如192.168.0.1)转换成32位的网络字节序二进制,并通过指针addrptr来存储。这个函数需要对字符串所指的地址进行有效性验证。但如果strptr为空,函数仍然成功,但不存储任何结果。

char *inet_ntoa(struct in_addr inaddr);

                                    返回:指向点分十进制数串的指针

函数inet_ntoa将32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。但由于返回值所指向的串留在静态内存中,这意味着函数是不可重入的。

需要注意的是这个函数是以结构为参数的,而不是指针。

上述三个地址转换函数都只能处理IPv4协议,而不能处理IPv6地址。

Tcp套接字



TCP套接字实现过程

服务器端步骤

1.创建套接字

2..绑定套接字

3..设置套接字为监听模式,进入被动接受连接请求状态

4..接受请求,建立连接

5.读/写数据

6.终止连接

客户端步骤

1.创建套接字

2.与远程服务程序连接

3.读/写数据

5.终止连接

基本套接字函数 - socket

socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。正如打电话之前,双方必须各自拥有一台电话机一样。在网间网内部,每一个socket用一个半相关描述:

(协议,本地地址,本地端口)

一个完整的socket有一个本地唯一的socket号,由操作系统分配。

对于基于TCP的通信,无论是服务器还是客户,都必须首先产生其TCP通信传输端点,即TCP套接字。

应用程序通过调用socket()产生套接字。该函数调用必须给出所使用的地址簇、套接字类型和协议标志。该函数返回一个套接字描述符。

由于系统中套接字也是一种文件,所以套接字描述符可以看成是一种文件描述符。之后的任何I/O操作都是作用于该套接字描述符。其数据结构包括一个网络连接的5种信息:通信协议、本地协议地址、本机主机端口、远程主机地址和远程协议端口。

socket函数:为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。


第一个参数指明了协议簇,目前支持5种协议簇,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议);第二个参数指明套接口类型,有三种类型可选:SOCK_STREAM(字节流套接口)、SOCK_DGRAM(数据报套接口)和SOCK_RAW(原始套接口);如果套接口类型不是原始套接口,那么第三个参数就为0。


基本套接字函数-bind

#include

int bind(int sockfd, const struct sockaddr *addr, socklen_len len)

                                返回:0-成功;-1-出错并置errno

该函数指明套接字将使用本地的哪一个协议端口进行数据传送(IP地址和端口号),注意:协议地址addr是通用地址。

Len是该地址结构(第二个参数)的长度。

一般而言,服务器调用此函数,而客户则很少调用它。

因为:客户端是主动向服务器发出请求的,客户开始发送数据,系统就给客户端分配一个随机的端口,这个端口和客户端的IP会随着数据一起发给服务器,服务器就可以从中或得客户的IP和端口,接下来服务器就可以利用获得的IP和端口给客户端回应消息。

bind函数用法


SO_REUSEADDR:允许套接口和一个已在使用中得地址绑定。一般缺省条件下一个套接口不能与一个已在使用中的本地地址绑定,但有时会需要“重用”地址。仅在bind()调用时该选项才被解释。

setsockopt函数 


返回:0-OK;-1-出错。sockfd必须指向一个打开的套接字描述字。Level是选项所在的层及协议,有如下值: SOL_SOCKET (通用套接字 ) IPPROTO_TCP(传输层,TCP协议) IPPROTO_IP(网际层,IP协议)Optname是所要操作的选项名。(SO_、IP_、TCP_是选项名的前缀,不同的level有不同的前缀。)optval是一个指向变量的指针,通过它设置选项的新值,此变量的大小由最后一个参数指定。

该函数用于任意类型、任意状态套接口的设置选项值,尽管在不同协议层上存在选项,但本函数定义了最高的“套接口”层次上得选项。选项影响套接口的操作,诸如:广播数据是否可以从套接口发送等等。

基本套接字函数-listen

listen函数:listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。


第一个参数是socket函数返回的套接口描述字;第二个参数规定了内核为此套接口排队的最大连接个数。由于listen函数第二个参数的原因,内核要维护两个队列:已完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手为完成的连接,accept函数是从已连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。

基本套接字函数-close

#include

int close(int sockfd);

                  返回:0-OK;-1-出错;

close函数缺省功能是将套接字做上“已关闭”标记,并立即返回到进程。这个套接字不能再为该进程所用。

正常情况下,close将引发向TCP的四分节终止序列,但在终止前将发送已排队的数据;

如果套接字描述符访问计数在调用close后大于0(在多个进程共享同一个套接字的情况下),则不会引发TCP终止序列(即不会发送FIN分节);

基本套接字函数-shutdown

#include

int shutdown(int sockfd, int howto);

                  返回:0-OK;-1-出错,并置相应的errno的值;

该函数立即发送FIN分节(无论其访问计数是否大于0)。shutdown根据参数howto关闭指定方向的数据传输;

SHUT_RD:关闭连接的读这一半,不再接收套接字中的数据且现留在接收缓冲区的数据作废;

SHUT_WR :关闭连接的写这一半(半关闭),当留在套接字发送缓冲区中的数据都被发送,后跟tcp连接终止序列,不管访问计数是否大于0;此后将不能在执行对套接字的任何写操作;

SHUT_RDWR:连接的读、写都关闭,这等效于调用shutdown两次,一次调用是用SHUT_RD,第二次用SHUT_WR。

基本套接字函数-read

#include

int read(int fd, char *buf, int len);

返回:大于0-读写字节大小;-1-出错;

调用函数read时,有如下几种情况:

套接字接收缓冲区接收数据,返回接收到的字节数;

tcp协议收到FIN数据,返回0;

tcp协议收到RST数据,返回-1,同时errno为ECONNRESET;

进程阻塞过程中接收到信号,返回-1,同时errno为EINTR。

read(connfd,buff,strlen(buff));

基本套接字函数-write

#include

int write(int fd, char *buf, int len);

                    返回:大于0-读写字节大小;-1-出错;

调用函数write,有如下几种情况:

套接字发送缓冲区有足够空间,返回发送的字节数;

tcp协议接收到RST数据,返回-1,同时errno为ECONNRESET; ;

进程阻塞过程中接收到信号,返回-1,同时errno为EINTR。

write(connfd,buff,strlen(buff));

数据传输函数-send

#include

#include

ssize_t send (int fd, const void *msg, size_t len, int flags);

      返回:非0-发送成功的数据长度;-1-出错;

flags 是传输控制标志,其值定义如下:

0:常规操作,如同write()函数

MSG_OOB,发送带外数据(TCP紧急数据)。

MSG_DONTROUTE:忽略底层协议的路由设置,只能将数据发送给与发送机处在同一个网络中的机器上。

数据传输函数-recv

#include

#include

ssize_t recv(int fd, void *buf ,size_t len, int flags);

            返回:大于0表示成功接收的数据长度;0: 对方已关闭,-1:出错。

flags是传输控制标志,其值定义如下:

0:常规操作,如同read()函数;

MSG_PEEK:只查看数据而不读出数据,后续读操作仍然能读出所查看的该数据;

MSG_OOB:忽略常规数据,而只读带外数据;

MSG_WAITALL:recv函数只有在将接收缓冲区填满后才返回。

域名解析函数-gethostbyname

#include

struct hostent *gethostbyname(const char *hostname)

    返回:非空指针-成功;空指针-出错,同时设置h_error

该函数既可解析IPv4地址,也可解析IPv6地址;

该函数既可接收域名,也可接收点分十进制参数

当hostname为点分十进制时,函数并不执行网络查询,而是直接将其拷贝到结果字段中。

此函数返回的非空指针指向下面的hostent结构



TCP服务器模板



TCP客户模板





UDP套接字

实现UDP套接字基本步骤分为服务器端和客户端两部分:

服务器端

1.建立UDP套接字;

2.绑定套接字到特定地址;

3.等待并接收客户端信息;

4.处理客户端请求;

5.发送信息回客户端;

6.关闭套接字;

客户端步骤

1.建立UDP套接字;

2.发送信息给服务器;

3.接收来自服务器的信息;

4.关闭套接字

UDP数据传输函数-sendto

#include

#include

ssize_t sendto(int sockfd, const void *msg, size_t len, int flags, const struct sockaddr *to, int tolen);

        返回:大于0-成功发送数据长度;-1-出错;

UDP套接字使用无连接协议,因此必须使用sendto函数,指明目的地址;

flags是传输控制标志,其值定义如下:

0:常规操作,如同write()函数;

MSG_OOB:发送带外数据;

MSG_DONTROUTE:忽略底层路由协议,直接发送。

UDP数据传输函数-recvfrom

#include

#include

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, int *fromlen);

          返回:大于0-成功接收数据长度;-1-出错;

UDP套接字使用无连接协议,因此必须使用recvfrom函数,指明源地址;

flags是传输控制标志,其值定义如下:

0:常规操作,如同read()函数;

MSG_PEEK:只察看数据而不读出数据;

MSG_OOB:忽略常规数据,而只读取带外数据;

from 和 fromlen 是“值-结果”参数。

UDP服务器模板



UDP客户模板


你可能感兴趣的:(【网络编程技术】基本套接字编程TCP)