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

  • 部分信息参考 中国石油大学 信息安全实验 信息安全实验四实验参考
  • 参考 信息安全实验资料 四个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 <netinet/in.h>
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 <sys/socket.h>
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 <sys/socket.h>
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 <netdb.h>
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<string.h>
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、网络字节顺序
  • 了解套接字的工作原理
  • 掌握套接字的通信过程
时间: 2024-11-05 13:51:38

C/C++ 网络编程3: 套接字基础的相关文章

[python] 网络编程之套接字Socket、TCP和UDP通信实例

很早以前研究过C#和C++的网络通信,参考我的文章: C#网络编程之Tcp实现客户端和服务器聊天 C#网络编程之套接字编程基础知识 C#网络编程之使用Socket类Send.Receive方法的同步通讯 Python网络编程也类似.同时最近找工作笔试面试考察Socket套接字.TCP\UDP区别比较多,所以这篇文章主要精简了<Python核心编程(第二版)>第16章内容.内容包括:服务器和客户端架构.套接字Socket.TCP\UDP通信实例和常见笔试考题. 最后希望文章对你有所帮助,如果有不

Linux网络编程——原始套接字实例:简单版网络数据分析器

通过<Linux网络编程--原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 MAC 头部(有线局域网) 注意:CRC.PAD 在组包时可以忽略 链路层数据包的其中一种情况: unsigned char msg[1024] = { //--------------组MAC--------14------ 0xb8, 0x88, 0xe3, 0xe1, 0x10, 0xe6, // dst

Linux网络编程——原始套接字实例:MAC 头部报文分析

通过<Linux网络编程——原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 MAC 头部(有线局域网) 注意:CRC.PAD 在组包时可以忽略 链路层数据包的其中一种情况: 1 unsigned char msg[1024] = { 2 //--------------组MAC--------14------ 3 0xb8, 0x88, 0xe3, 0xe1, 0x10, 0xe6,

TCP/IP网络编程之套接字类型与协议设置

套接字与协议 如果相隔很远的两人要进行通话,必须先决定对话方式.如果一方使用电话,另一方也必须使用电话,而不是书信.可以说,电话就是两人对话的协议.协议是对话中使用的通信规则,扩展到计算机领域可整理为"计算机间对话必备通信规则" 在TCP/IP网络编程之网络编程和套接字这一章中,我们已经介绍了如何创建套接字,但为了完全理解该函数,此处将继续展开讨论 #include <sys/socket.h> int socket(int domain, int type, int pr

网络编程--Socket(套接字)

网络编程 网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯.网络编程中 有两个主要的问题,一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后 如何可靠高效的进行数据传输.在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的 路由,由IP地址可以唯一地确定Internet上的一台主机.而TCP层则提供面向应用的可靠的 或非可靠的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据 的. 目前较为流行的网络编程模型是客户机/服务器(C/S)结构

《网络编程》套接字编程简介

本节介绍的套接字是可以实现不同计算机之间的远程进程间通信.套接口是网络进程的 ID,在网络中每一个节点都有一个网络地址,也就是 IP 地址,两个进程间通信时,首先要确定各自所在网络节点的网络地址.但是,网络地址只要确定进程所在的计算机,由于一台计算机上同时可能有多个网络进程,所以仅凭网络地址还不能确定是网络中的哪一个进程,因此套接口中还需要其他信息,也就是端口.在一台计算机中,一个端口号只能分配给一个进程,所以,进程和端口之间是一一对应的关系.因此,使用端口号和网络地址的组合就能唯一地确定整个网

网络编程之套接字socket

目录 socket套接字 引子 为何学习socket一定要先学习互联网协议 socket是什么 套接字类型 基于文件类型的套接字家族 基于网络类型的套接字家族 套接字工作流程 基于TCP的套接字 简单通信 加上链接循环与通信循环 基于UDP的套接字 UDP的套接字下的简单通信 UDP协议支持并发 粘包现象 什么是粘包 两种情况下会发生粘包 解决粘包问题的处理方法 简单方法(不推荐使用) 牛逼方法(利用struct模块打包报头) socketserver模块(实现并发) socketserver模

(一)理解网络编程和套接字

学习<TCP/IP网络编程> 韩 尹圣雨 著 金国哲 译 套接字类似电话 一.服务器端套接字(listening套接字)---接电话套接字 ①调用socket函数---安装电话机 #include <sys/socket.h> int socket(int domain, int type, int protocol); //成功时返回文件描述符,失败时返回-1 ②调用bind函数---分配电话号码 #include <sys/socket.h> int bind(in

【转】网络编程原始套接字

转自:http://www.cnblogs.com/hnrainll/archive/2011/09/20/2182423.html SOCKET_STREAM 流式套接字      SOCKET_DGRAM        SOCKET_RAW 原始套接字    IPPROTO_IP IP协议    IPPROTO_ICMP INTERNET控制消息协议,配合原始套接字可以实现ping的功能    IPPROTO_IGMP INTERNET 网关服务协议,在多播中用到 在AF_INET地址族下,