欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C/C++ 网络编程3: 套接字基础

程序员文章站 2022-05-03 12:33:44
部分信息参考 中国石油大学 信息安全实验 信息安全实验四实验参考 参考 信息安全实验资料 四个ppt文件 server.c 套接字地址 linux的套接字可以支持多种协议,每种不同的协议都是用不...
部分信息参考 中国石油大学 信息安全实验 信息安全实验四实验参考 参考 信息安全实验资料 四个ppt文件 server.c

套接字地址

linux的套接字可以支持多种协议,每种不同的协议都是用不同的地址结构。 在头文件中定义了一个通用套接字地址结构sockaddr:
struct sockaddr
{
    unsigned short sa_family; //16位 套接字的协议簇地址类型,af_xx
    char        sa_data[14];//14字节 存储具体的协议地址
};
为了处理struct sockaddr,程序员创造了一个并列的大小相同结构:struct sockaddr_in(“in”代表”internet”。) struct sockaddr_in在/usr/include/netinet/in.h中定义:
struct sockaddr_in
{
    unsigned short sin_len;  //ipv4地址长度
    short int sin_family;   //16位 指代协议簇,tcp套接字编程为af_inet
    unsigned short sin_port;     //16位端口号(使用网络字节顺序),数据类型是一个16位的无符号整数
    struct in_addr sin_addr;   //32位,存储ip地址,是一个in_addr结构体
    unsigned char sin_zero[8];     //8字节,为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
};
struct in_addr //32 位
{
    unsigned long s_addr;  //按照网络字节顺序存储ip地址
};
填充特定协议地址时使用sockaddr_in 作为bind()、connect()、sendto()、recvfrom()等函数的参数时需要使用sockaddr, 这时要通过指针强制转换的方式转为struct sockaddr 指针。

ipv4地址结构示例

struct sockaddr_in mysock;
mysock.sin_family = af_inet;  //tcp地址结构
mysock.sin_port = htons(3333); //字节顺序转换函数
mysock.sin_addr.s_addr = inet_addr("166.111.160.10"); //设置ip地址
//如果mysock.sin_addr.s_addr = inaddr_any,则不指定ip地址(用于server程序)
bzero(&(mysock.sin_zero),8); //设置sin_zero为8位保留字节

ipv6套接字地址结构sockaddr_in6

#define sin6_len
struct sockaddr_in6
{
    unsigned short  sin6_len;        //16位 ipv6地址长度,是一个无符号的8位整数,表示128位的ipv6地址
    short int   sin6_family;   //16位 地址类型为af_inet6
    unsigned short  sin6_port;     //16位 存储端口号,使用网络字节顺序
    unsigned short int sin6_flowinfo;  //低24位是流量标号,然后是4位优先级标志,剩下4位保留
    struct in6_addr sin6_addr;   //ipv6地址,网络字节顺序
};
struct in6_addr
{
    unsigned long   s6_addr;  //网络字节顺序的ipv6地址
};

ip地址转换函数

inet_aton():将字符串形式的ip地址转换成二进制形式的ip地址,成功返回1,否则返回0,转换后的ip地址存储在参数inp中。 inet_ntoa():将32位二进制形式的ip地址转换为数字点形式的ip地址,结果在函数返回值中返回。
unsigned long inet_aton(const char *cp, struct in_addr *inp);
char* inet_ntoa(struct in_addr in);

网络字节顺序

字节序,顾名思义字节的顺序,就是大于一个字节的数据在内存中的存放顺序。 在跨平台以及网络程序应用中字节序才是一个应该被考虑的问题。 网络字节序是tcp/ip规定的一种数据表示格式,与具体的cpu类型、操作系统无关,从而可以保证数据在不同主机之间传输时能被正确解释。网络字节顺序采用big endian(大端字节序)。 intel x86系列cpu使用的都是little endian(小端字节序) 大端字节序(big-endian):低地址存放最高有效字节 小端字节序(little-endian):低地址存放最低有效字节 例如数字0x12345678(dword)在两种不同字节序cpu中的存储顺序如下所示:

字节顺序转换函数

下面四个函数分别用于长整型和短整型数在网络字节序和主机字节序之间进行转换,其中s指short,l指long,h指host,n指network
#include 
unsigned long htonl(unsigned long host_long);
unsigned short htons(unsigned short host_short);
unsigned long ntohl(unsigned long net_long);
unsigned short ntohs(unsigned short net_short);

什么时候要考虑字节序问题

如果是应用层的数据,即对tcp/ip来说是透明的数据,不用考虑字节序的问题。因为接收端收到的顺序是和发送端一致的 但对于tcp/ip的ip地址、端口号来说就不一样了,例如
unsigned short prot = 0x0012  //十进制18
struct sockaddr_in mysock;
mysock.sin_family = af_inet;  //tcp地址结构
mysock.sin_port = prot;
因为网络字节序是big endian,即低地址存放的是数值的高位,所以tcp/ip实际上把这个port解释为0x1200(十进制4608)。 本来打算是要在端口18建立连接的,但tcp/ip协议栈却在端口4608建立了连接

套接字的工作原理

inet 套接字就是支持 internet 地址族的套接字,它位于tcp协议之上,bsd套接字之下, 如图所示,这里也体现了linux网络模块分层的设计思想(图在ppt里,自己想象吧…) inet和 bsd 套接字之间的接口通过 internet 地址族套接字操作集实现,这些操作集实际是一组协议的操作例程, 在include/linux/net.h中定义为proto_ops:
struct proto_ops {
    int family; 
    int (*release) (struct socket *sock);
    int (*bind) (struct socket *sock, struct sockaddr *umyaddr,   int sockaddr_len);
    int (*connect) (struct socket *sock, struct sockaddr *uservaddr, int sockaddr_len, int flags);
    int (*socketpair) (struct socket *sock1, struct socket *sock2);
    int (*accept) (struct socket *sock, struct socket *newsock, int flags);
    int (*getname) (struct socket *sock, struct sockaddr *uaddr, int *usockaddr_len, int peer);
    unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait);
    int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
    int (*listen) (struct socket *sock, int len);
    int (*shutdown) (struct socket *sock, int flags);
    int (*setsockopt) (struct socket *sock, int level, int optname, char *optval, int optlen);
    int (*getsockopt) (struct socket *sock, int level, int optname, char *optval, int *optlen);
    int (*sendmsg) (struct socket *sock, struct msghdr *m, int  total_len, struct scm_cookie *scm);
    int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int flags, struct scm_cookie *scm);
    int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma);
    ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags);
};
这个操作集类似于文件系统中的file_operations结构。bsd套接字层通过调用proto_ops 结构中的相应函数执行任务。 bsd套接字层向 inet 套接字层传递socket数据结构来代表一个bsd套接字,

socket 定义

socket结构在include/linux/net.h中定义:
struct socket {
   socket_state state;
   unsigned long flags;
   struct proto_ops *ops;
   struct inode *inode;
   struct fasync_struct *fasync_list; /* asynchronous wake up list */
   struct file *file;       /* file back pointer for gc */
   struct sock *sk;
   wait_queue_head_t wait;
   short type;
   unsigned char passcred;
};
但在inet套接字层中,它利用自己的sock数据结构来代表该套接字,因此,这两个结构之间存在着链接关系 sock结构定义于include/net/sock.h(此结构有80多行,在此不予列出 在bsd的socket数据结构中存在一个指向sock的指针sk,而在sock中又有一个指向socket的指针, 这两个指针将bsd socket数据结构和sock数据结构链接了起来。 通过这种链接关系,套接字调用就可以方便地检索到sock数据结构 实际上,sock数据结构可适用于不同的地址族,它也定义有自己的协议操作集proto 进程在利用套接字进行通讯时,采用客户-服务器模型。服务器首先创建一个套接字,并将某个名称绑定到该套接字上,套接字的名称依赖于套接字的底层地址族,但通常是服务器的本地地址

/etc/services 文件

对于inet套接字来说,服务器的地址由两部分组成:服务器的ip地址和服务器的端口地址。已注册的标准端口可查看/etc/services 文件 将地址绑定到套接字之后,服务器就可以监听请求连接该绑定地址的传入连接 连接请求由客户生成,它首先建立一个套接字,并指定服务器的目标地址以请求建立连接 传入的连接请求通过不同的协议层到达服务器的监听套接字 服务器接收到传入请求后,如果能够接受该请求,服务器必须创建一个新的套接字来接受该请求并建立通信连接(用于监听的套接字不能用来建立通信连接),这时,服务器和客户就可以利用建立好的通信连接传输数据 bsd套接字上的详细操作与具体的底层地址族有关,底层地址族的不同实际意味着寻址方式、采用的协议等的不同 linux 利用bsd套接字层抽象了不同的套接字接口。在内核的初始化阶段,内建于内核的不同地址族分别以bsd套接字接口在内核中注册 然后,随着应用程序创建并使用bsd套接字 内核负责在bsd套接字和底层的地址族之间建立联系。这种联系通过交叉链接数据结构以及地址族专有的支持例程表建立 在内核中,地址族和协议信息保存在inet_protos向量中,其定义于include/net/protocol.h
struct inet_protocol *inet_protos[max_inet_protos];
/* this is used to register protocols. */
struct inet_protocol {
    int   (*handler)(struct sk_buff *skb);
    void  (*err_handler)(struct sk_buff *skb, u32 info);
    struct inet_protocol    *next;
    unsigned char protocol;
    unsigned char copy:1;
    void      *data;
    const char    *name;
};

建立套接字

linux在利用socket()系统调用建立新的套接字时,需要传递套接字的地址族标识符、套接字类型以及协议,其函数定义于net/socket.c中
asmlinkage long sys_socket(int family, int type, int protocol) {
  int retval;
  struct socket *sock;
  retval = sock_create(family, type, protocol, &sock);
  if (retval < 0)
     goto out;
  retval = sock_map_fd(sock);
  if (retval < 0)
     goto out_release;
 out:
      /* it may be already another descriptor 8) not kernel problem. */
     return retval;
 out_release:
     sock_release(sock);
     return retval;
}

sockfs

实际上,套接字对于用户程序而言就是特殊的已打开的文件。内核中为套接字定义了一种特殊的文件类型,形成一种特殊的文件系统sockfs 所谓创建一个套接字,就是在sockfs文件系统中创建一个特殊文件,或者说一个节点,并建立起为实现套接字功能所需的一整套数据结构 所以,函数sock_create()首先是建立一个socket数据结构,然后将其“映射”到一个已打开的文件中,进行socket结构和sock结构的分配和初始化 实际上,socket结构与sock结构是同一事物的两个方面。如果说socket结构是面向进程和系统调用界面的,那么sock结构就是面向底层驱动程序的 把与文件系统关系比较密切的那一部分放在socket结构中,把与通信关系比较密切的那一部分则单独组成一个数据结构,即sock结构 由于这两部分数据在逻辑上本来就是一体的,所以要通过指针互相指向对方,形成一对一的关系

在inet bsd套接字上绑定(bind)地址

为了监听传入的internet 连接请求,每个服务器都需要建立一个inet bsd套接字,并且将自己的地址绑定到该套接字 将地址绑定到某个套接字上之后,该套接字就不能用来进行任何其他的通信,因此,该socket数据结构的状态必须为tcp_close 传递到绑定操作的sockaddr数据结构中包含要绑定的 ip地址以及一个可选的端口地址。被绑定的ip地址保存在sock数据结构的rcv_saddr和 saddr域中,这两个域分别用于哈希查找和发送用的ip地址。 端口地址是可选的,如果没有指定,底层的支持网络会选择一个空闲的端口 当底层网络设备接受到数据包时,它必须将数据包传递到正确的 inet 和 bsd 套接字以便进行处理,因此,tcp维护多个哈希表,用来查找传入 ip 消息的地址,并将它们定向到正确的socket/sock 对 tcp 并不在绑定过程中将绑定的sock数据结构添加到哈希表中,在这一过程中,它仅仅判断所请求的端口号当前是否正在使用。在监听操作中,该 sock 结构才被添加到 tcp 的哈希表中

在inet bsd套接字上建立连接 (connect)

创建一个套接字之后,该套接字不仅可以用于监听入站的连接请求,也可以用于建立出站的连接请求。不论怎样都涉及到一个重要的过程:建立两个应用程序之间的虚拟电路。出站连接只能建立在处于正确状态的 inet bsd 套接字上, 因此,不能建立于已建立连接的套接字,也不能建立于用于监听入站连接的套接字。也就是说,该 bsd socket 数据结构的状态必须为 ss_unconnected 在建立连接过程中,双方 tcp 要进行三次“握手”。如果 tcp sock 正在等待传入消息,则该 sock 结构添加到 tcp_listening_hash 表中,这样,传入的 tcp 消息就可以定向到该 sock 数据结构

监听(listen) inet bsd 套接字

当某个套接字被绑定了地址之后,该套接字就可以用来监听专属于该绑定地址的传入连接。网络应用程序也可以在未绑定地址之前监听套接字,这时,inet 套接字层将利用空闲的端口编号并自动绑定到该套接字。套接字的监听函数将 socket 的状态改变为tcp_listen 当接收到某个传入的 tcp 连接请求时,tcp 建立一个新的 sock 数据结构来描述该连接。当该连接最终被接受时,新的 sock 数据结构将变成该 tcp 连接的内核bottom_half部分,这时,它要克隆包含连接请求的传入 sk_buff 中的信息,并在监听 sock 数据结构的 receive_queue 队列中将克隆的信息排队。克隆的 sk_buff 中包含有指向新 sock 数据结构的指针

接受连接请求(accept)

接受操作在监听套接字上进行,从监听 socket 中克隆一个新的 socket 数据结构。其过程如下: 接受操作首先传递到支持协议层,即inet中,以便接受任何传入的连接请求。接受操作可以是阻塞的或是非阻塞的。非阻塞时,若没有可接受的传入连接,则接受操作将失败,而新建立的socket数据结构被抛弃。阻塞时,执行阻塞操作的网络应用程序将添加到等待队列中并保持挂起直到接收到一个tcp连接请求为止。 当连接请求到达之后,包含连接请求的sk_buff被丢弃,而由tcp建立的新sock数据结构返回到inet套接字层,在这里,sock数据结构和先前建立的新socket数据结构建立链接。而新socket的文件描述符(fd)被返回到网络应用程序,此后,应用程序就可以利用该文件描述符在新建立的inet bsd套接字上进行套接字操作

套接字为用户提供的系统调用

见ppt 3.7

getsockname()函数

getsockname(): 获取与当前套接字绑定的ip地址及端口
#include 
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:成功返回0,失败返回-1,并在errno中设置错误代码。 错误代码:
ebadf :
the argument sockfd is not a valid descriptor.
efault :
the addr argument points to memory not in a valid part of the process address space.
einval :
addrlen is invalid (e.g., is negative).
enobufs :
insufficient resources were available in the system to perform the operation.
enotsock :
the argument sockfd is a file, not a socket.

getpeername()函数

#include 
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:成功返回0,失败返回-1,并在errno中设置错误代码。 错误代码:
ebadf
the argument sockfd is not a valid descriptor.
efault
the addr argument points to memory not in a valid part of the process address space.
einval
addrlen is invalid (e.g., is negative).
enobufs
insufficient resources were available in the system to perform the operation.
enotconn
the socket is not connected.
enotsock
the argument sockfd is a file, not a socket.

gethostbyname()和gethostbyaddr()

gethostbyname():主机名转换为ip地址 gethostbyaddr():ip地址转换成主机名
#include 
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
struct hostent {
       char    *h_name;           /* 主机的正式名称 */
       char    **h_aliases;       /* 主机别名列表 */
       int       h_addrtype;        /* 主机地址类型 */
       int       h_length;            /* 地址长度 */
       char    **h_addr_list;    /* 地址列表 */
};
#define     h_addr h_addr_list[0] /* 保持后向兼容 */

getservbyname()和getservbyport()

getservbyname():根据给定名字查找相应服务,返回服务的端口号 getservbyport():给定端口号和可选协议查找相应服务
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
struct servent {
    char *s_name; /* official service name */
    char **s_aliases; /* alias list */
    int s_port; /* port number */
    char *s_proto; /* protocol to use */
}

字节处理函数 bzero bcopy bcmp memset memcpy memcmp

套接字地址是多字节数据,不是以空字符结尾的,linux提供两组函数来处理多字节数据。一组函数以b开头,适合bsd系统兼容的函数;另一组函数以mem开头,是ansi c提供的函数
#include
void bzero(void *s,int n);
void bcopy(const void *src,void *dest,int n);
void bcmp(const void *s1,const void *s2,int n);
void *memset(void *s,int c,size_t n);
void *memcpy(void *dest,void *src,size_t n);
void memcmp(const void *s1, const void *s2,size_t n);
函数bzero将参数s指定的内容的前n个字节设置为0,通常用它来将套接字地址清零 函数bcopy从参数src指定的内存区域拷贝指定数目的字节内容到参数dest指定的内存区域 函数bcmp比较参数s1指定的内存区域和参数s2指定的内存区域的前n个字节内容,相同则返回0,否则返回非0 将参数s指定的内存区域的前n个字节设置为参数c的内容 类似于bcopy,但bcopy能处理参数src和参数dest所指定的区域有重叠的情况,而memcpy不能 比较参数s1和参数s2指定区域的前n个字节内容,相同则返回0,否则返回非0

小结

套接字标识tcp/ip的连接 使用套接字要注意: 1、sockaddr与sockaddr_in的区别 2、网络字节顺序 了解套接字的工作原理 掌握套接字的通信过程