Linux网络编程(二)套接字编程

(一)套接字地址结构

结构struct sockaddr定义了一种通用的套接字地址,它在sys/socket.h中的定义代码如下:

struct sockaddr {
  // sa_family_t:__uint16_t
  // sa:         Socket Address
  sa_family_ts a_family;  /* address family, AF_xxx         */
  char sa_data[14];	      /* 14 bytes of protocol address	*/
};
  • 成员sa_family表示套接字的协议族类型。
    • AF_INET:TCP/IP
  • 成员sa_data储存具体的协议地址。
  • 被定义为14字节是因为有的地址具有较长的地址格式。
  • 在一般的编程中并不对该结构体进行操作,而使用另一个与它等价的数据结构:sockaddr_in。

每种协议族都有自己的协议地址格式。TCP/IP协议族的地址格式为结构体struct sockaddr_in,其在netinet/in.h中的定义代码如下:

/* Structure describing an Internet (IP) socket address. */
#define __SOCK_SIZE__	16		/* sizeof(struct sockaddr)	*/
struct sockaddr_in
{
    // sa_family_t: __uint16_t
    // in_port_t:   __uint16_t
    // sin_:        socket_in
    sa_family_t	 sin_family;	/* Address family		    */
    in_port_t	 sin_port;	    /* Port number			    */
    struct in_addr sin_addr;	/* Internet address		    */

    /* Pad to size of `struct sockaddr'. */
    unsigned char  __pad[__SOCK_SIZE__ - sizeof(short int)
                         - sizeof(unsigned short int) - sizeof(struct in_addr)];
};

其结构体成员的定义也可以描述成这样(地址类型、端口号、IP地址和填充字节):

struct sockaddr_in {
    unsigned short	     sin_family;  // Address family
    unsigned short int	 sin_port;    // Port number
    struct in_addr       sin_addr;    // Internet address
    unsigned char        sin_zero[8]; // Padding bytes, default 0
};

而其中的用于IP地址的成员sin_addr的结构类型定义代码是这样的(32位无符号整数):

/* Internet address. */
struct in_addr
{
    // in_addr_t: __uint32_t
    in_addr_t s_addr;
};

可以发现,结构体sockaddr结构体sockaddr_in的长度均为16字节

通常编写基于TCP/IP协议的网络程序时,使用结构体sockaddr_in来设置地址,并通过强制类型转换scokaddr类型

给出一般性设置地址信息的示例代码:

#include <string.h>
#include <arpa/inet.h>

struct sockaddr_in sock;
sock.sin_family = AF_INET;     /* IPv4                           */
sock.sin_port  = htons(80);    /* Port number 80                 */
sock.sin_addr.s_addr = inet_addr("202.205.3.195"); /* IP address */
memset(sock.__pad, 0, sizeof(sock.__pad)); /* Array zeroing      */
(二)创建套接字

socket函数用于创建一个套接字。

#include <sys/types.h>
#include <sys/socket.h>

int socket (int __family, int __type, int __protocol);
  • __family:套接字的协议族
    • AF_UNIX :仅本机内通信
    • AF_INET :IPv4协议
    • AF_INET6 :IPv6协议
  • __type:套接字的类型
    • SOCK_STREAM:TCP流式
    • SOCK_DGRAM :UDP数据报式
    • SOCK_RAW :原始类型
  • __protocol:指定所用的协议
    • 0:通过*__family__type*来确定使用的协议
    • 创建原始套接字时,系统无法唯一地确定协议,就需要使用该参数
  • 函数返回值
    • 正常:0
    • 错误:-1
      • 错误代码存入errno

如下创建一个TCP套接字UDP套接字

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>

int sock_fd_TCP = socket(AF_INET, SOCK_STREAM, 0);
if(sock_fd_TCP < 0) {
    perror("socket");
    exit(1);
}

/*
int sock_fd_UDP = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_fd_UDP < 0) {
    perror("socket");
    exit(1);
}
*/
(三)建立连接

connect函数用来在一个指定的套接字上创建一个连接。

#include <sys/types.h>
#include <sys/socket.h>

int connect (int, const struct sockaddr *, socklen_t);
  • 第一个参数sockfd(int)是由socket函数创建的套接字
    • SOCK_STREAM:connect函数向服务器发出连接请求,服务器的IP地址端口号由第二个参数*serv_addr(const struct sockaddr *)*指定
    • SOCK_DGRAM:connect函数并不建立真正的连接,仅告诉内核与该套接字进行通信的目的地址,只有该目的地址发来的数据才会被该socket接收
      • 好处是不必在每次发送和接收数据时都指定目的地址
  • 第二个参数*serv_addr(const struct sockaddr *)*是通用套接字地址结构类型
  • 第三个参数*addrlen(socklen_t)是第二个参数serv_addr(const struct sockaddr *)*的长度
  • 函数返回值:成功返回0;有错误返回-1,并将代码存入errno

通常一个面向连接的套接字(TCP套接字)只能调用一次connect函数,而面向无连接的套接字(UDP套接字)可以多次调用connect函数,以改变与目的地址的绑定(将第二个参数serv_addr(const struct sockaddr *)中的sa_family设置为AF_UNSPEC)。

下面是connect函数的一般用法:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port  = htons(80);

// 将一个字符串转换为网络地址,并赋值给第二个参数
if(inet_aton("127.17.242.131", &server_addr.sin_addr) < 0) {
    perror("inet_aton");
    exit(1);
}

// 使用sock_fd套接字连接到由server_addr指定的目的地址上
if(connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) < 0) {
    perror("connect");
    exit(1);
}

(四)绑定套接字

socket函数只是创建了一个套接字,并没有指定这个套接字将工作在哪个端口上。在C/S模型中,服务器端的IP地址和端口号一般是固定的,因此在服务器端的程序中,使用bind函数来将一个套接字和某个端口绑定在一起(该函数一般只有服务器端的程序调用)。

函数bind用来将一个套接字和某个端口绑定在一起。

#include <sys/types.h>
#include <sys/socket.h>

int bind (int, const struct sockaddr *__my_addr, socklen_t __addrlen);
  • 第一个参数sockfd(int)指定了sockfd将要绑定到的本地地址
    • 可以将第二个参数*__my_addrsin_addr设置为INADDR_ANY*而不是某个确定的IP地址就可以绑定到任何网络接口
  • 函数返回值:成功返回0;错误返回-1,并将代码存入errno

该函数的常见用法如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port  = htons(80);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

if(bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) < 0) {
    perror("bind");
    exit(1);
}
(五)在套接字上监听

函数socket创建的套接字是主动套接字,这种套接字可以用来主动请求连接到某个服务器(connect函数)。在服务器端,一般是先调用函数socket创建一个主动套接字,然后调用函数bind将该套接字绑定到某个端口上,接着再调用函数listen将该套接字转为监听套接字,等待来自客户端的连接请求。

函数listen把套接字转换为被动监听,其本身并不能接收连接请求。

#include <sys/socket.h>

int listen (int, int __n);

一般多个客户端连接到一个服务器,服务器向这些客户端提供某种服务。服务器端设置一个连接队列,记录已经建立的连接,第二个参数*__n(int)*指定了该连接队列的最大长度。如果连接队列已达最大,则之后的连接请求将被服务器端拒绝。

函数返回值:成功返回0;错误返回-1,并将代码存入errno中。

该函数最常见的用法如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>

#define LISTEN_NUM 12

if(listen(sock_fd, LISTEN_NUM) < 0) {
    perror("listen");
    exit(1);
}
(六)接受连接

函数accept用来接受一个连接请求(只能用于面向连接的套接字——例如TCP)。

#include <sys/types.h>
#include <sys/socket.h>

int accept (int, struct sockaddr *__peer, socklen_t *);
  • 第一个参数s(int)是由函数socket创建的,经过函数bind绑定到本地某个端口上,接着通过函数listen转化而来的监听套接字
  • 第二个参数*__peer(struct sockaddr *)*用来保存发起连接请求的主机的地址和端口
  • 第三个参数*addrlen(socklen_t *)*是所指结构体的大小
  • 函数返回值
    • 成功:创建并返回一个新的代表客户端的套接字
    • 出错:错误返回-1,并将代码存入errno

进程利用返回的这个新的套接字描述符与客户端交换数据,第一个参数*s(int)*所指定的套接字继续等待客户端的连接请求。

  • 如果第一个参数s(int)所指定的套接字被设置为阻塞方式(Linux下默认),且连接请求队列为,则*函数accept***将被阻塞直到有连接请求到达为止。
  • 如果第一个参数s(int)所指定的套接字被设置为非阻塞方式,且连接请求队列为,则函数accept将立即返回-1errno被设置为EAGAIN。

套接字为阻塞方式下,该函数的常见用法如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int client_fd;
int client_len;
struct sockaddr_in client_addr;
...
client_len = sizeof(struct sockaddr_in);
clinet_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &client_len);
if(conn_fd < 0) {
    perror("accept");
    exit(1);
}

(七)TCP套接字的数据传输

(1)发送数据

函数send用来在TCP套接字上发送数据,但只能对处于连接状态的套接字使用。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send (int, const void *__buff, size_t __len, int __flags);
  • 第一个参数s(int)是已经建立连接的套接字描述符,即函数accept的返回值。
  • 第二个参数*__buff(const void *)*指向存放待发送数据的缓冲区。
  • 第三个参数*__len(size_t)*为待发送数据的长度。
  • 第四个参数*__flags(int)*为控制选项
    • 0:默认值
    • MSG_OOB:在指定的套接字上发送带外数据(out-of-band data),该类型的套接字必须支持带外数据(如SOCK_STREAM)
    • MSG_DONTROUTE:通过最直接的路径发送数据,忽略下层协议的路由设置
  • 函数返回值:执行成功返回实际发送数据的字节数;错误则返回-1,并将代码存入errno中。

如果要发送的数据太长而无法发送时,将出现错误,errno设置为EMSGSIZE

如果要发送的数据长度大于该套接字的缓冲区剩余空间大小时,send函数一般会被阻塞。

如果该套接字被设置为非阻塞方式,则此时立即返回-1,并将errno设置为EAGAIN

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>

#define BUFFERSIZE 1500
char send_buf[BUFFERSIZE];

if(send(conn_fd, sned_buf, len, 0) < 0) {
    perror("sned");
    exit(1);
}

(二)接收数据

函数recv用来在TCP套接字上接收数据。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv (int, void *__buff, size_t __len, int __flags);
  • 函数从第一个参数*s(int)指定的套接字描述符上接收数据并保存到第二个参数__buff(const void *)*所指向的数据缓冲区。
  • 第三个参数*__len(size_t)*为缓冲区长度。
  • 第四个参数*__flags(int)*为控制选项
    • 0:默认值
    • MSG_OOB:请求接收带外数据
    • MSG_PEEK:只查看数据而不读出
    • MSG_WIATALL:只在接收缓冲区满时才返回
  • 函数返回值:执行成功返回实际接收数据的字节数;错误则返回-1,并将代码存入errno中。

如果一个数据包太长以至于缓存区不能完全放下时,剩余部分的数据可能被丢弃(视套接字类型而定)。

如果在指定的套接字上无数据到达时,函数revc将被阻塞。如果该套接字被设置为非阻塞方式,则立即返回-1并将errno设置为EAGAIN

函数recv接收到数据就返回,并不会等待接收到第二个参数*__len*指定长度的数据才返回。

套接字为阻塞方式下该函数的一般用法如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>

#define BUFFERSIZE 1500
char recv_buf[BUFFERSIZE];

if(recv(conn_fd, recv_buf, sizeof(recv_buf), 0) < 0) {
    perror("recv");
    exit(1);
}
(八)UDP套接字的数据传输

(1)发送数据

函数sendto同来在UDP套接字上发送数据,其功能与函数send类似,但函数sendto不需要套接字处于连接状态。因为是无连接的套接字,在使用sendto函数时需要指定数据的目的地址。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto (int, const void *, size_t __len, int __flags,
		  const struct sockaddr *__to, socklen_t __tolen);
  • 第二个参数*msg(const void *)*指向待发送数据的缓存区。
  • 第三个参数*__len(size_t)*指定待发送数据的长度。
  • 第四个参数*__flags(int)是控制选项,含义和send函数*一致。
  • 第五个参数*__to(const struct sockaddr *)*指定目的地址。
  • 第六个参数*__tolen(socklen_t)*指定目的地址的长度。
  • 函数返回值:执行成功返回实际发送数据的字节数;错误则返回-1,并将代码存入errno中。

以下为该函数的常见用法:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUFFERSIZE 1500
char send_buf[BUFFERSIZE];

struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(struct sockaddr_in));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(DEST_PORT);

if(inet_aton("127.17.242.131", &dest_addr.sin_addr) < 0) {
    perror("inet_aton");
    exit(1);
}

if(sendto(sock_fd, send_buf, len, 0, (struct sockaddr *)&dest_addr), sizeof(struct sockaddr_in) < 0) {
    perror("sendto");
    exit(1);
}

(2)接收数据

函数recvfrom用来在UDP套接字上接收数据,其功能与函数recv类似,但recv函数只能用于面向连接的套接字,而recvfrom函数可以用于从无连接的套接字上接收数据。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom (int, void *__buff, size_t __len, int __flags,
		    struct sockaddr *__from, socklen_t *__fromlen);
  • 第二个参数*__buff(void *)*指向接收数据的缓存区。
  • 第三个参数*__len(size_t)*指定缓存区的大小。
  • 第四个参数*__flags(int)是控制选项,含义和recv函数*一致。
  • 第五个参数*__from(struct sockaddr *)非空且该套接字不是面向连接的,则函数recvfrom*返回时,这个参数将保存数据的源地址。
  • 第六个参数*__fromlen(socklen_t)recvfrom函数调用前为第五个参数__from的长度,调用recvfrom函数后将保存第五个参数__from*的实际大小。
  • 函数返回值:执行成功返回实际接收数据的字节数;错误则返回-1,并将代码存入errno中。
#include <sys/types.h>
#include <sys/socket.h>

#define BUFFERSIZE 1500
char recv_buf[BUFFERSIZE];

struct sockaddr_in src_addr;
int src_len = sizeof(struct sockaddr_in);

if(recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&src_addr, &src_len) < 0) {
    perror("again_recvfrom");
    exit(1);
}
(九)关闭套接字

(1)函数close

函数close用来关闭一个套接字描述符,它与关闭文件描述符类似。

#include <unistd.h>

int close (int __fildes);
  • 参数*__fildes*为一个套接字描述符。
  • 函数返回值:执行成功返回0;错误则返回-1,并将代码存入errno中。

(2)函数shutdown

函数shutdown用于关闭一个套接字描述符,其功能比函数close更强大,能够对关闭套接字进行更细致的控制,其允许对套接字进行单项关闭或全部禁止。

#include <sys/socket.h>

int shutdown (int, int);
  • 第一个参数*s(int)*为待关闭的套接字描述符
  • 第二个参数*how(int)*指定了关闭的方式
    • SHUT_RD:关闭读通道
    • SHUT_WR:关闭写通道
    • SHUT_RDWA:关闭读、写通道
  • 函数返回值:执行成功返回0;错误则返回-1,并将代码存入errno中。
(十)主要系统调用函数

(1)字节顺序和转换函数

不同机器内部对变量的字节储存顺序不同。

  • 大端模式(big-endian)
    • 高字节数据存放在低地址处
    • 低字节数据存放在高地址处
  • 小端模式(little-endian)
    • 高字节数据存放在高地址处
    • 低字节数据存放在低地址处

由于数据传输平台两端的存储字节顺序可能不一致,为此,TCP/IP协议规定了在网络上必须采用网络字节顺序(大端模式)。

对于char类型数据,由于其只占1字节,所以不存在这个问题,这也是缓存区定义为char类型的原因之一。

Linux系统为大小端模式转换提供了4个函数。

#include <arpa/inet.h>

// host to network long
// host to network short
extern uint32_t	htonl(uint32_t);
extern uint16_t	htons(uint16_t);

// network to host long
// network to host short
extern uint32_t	ntohl(uint32_t);
extern uint16_t	ntohs(uint16_t);

(2)inet系统函数

在网络上进行数据通信时,需要使用的是二进制形式且为网络字节顺序的IP地址。

  • "172.17.242.131"对应的二进制形式为:0x83f211ac

Linux系统为网络地址的格式转换提供了一系列函数。

#include <arpa/inet.h>

in_addr_t inet_addr (const char *);             // *
int inet_aton (const char *, struct in_addr *); // **
in_addr_t inet_lnaof (struct in_addr);
struct in_addr inet_makeaddr (unsigned long , unsigned long);
in_addr_t inet_netof (struct in_addr);
in_addr_t inet_network (const char *);
char *inet_ntoa (struct in_addr);
int inet_pton (int, const char *, void *);
const char *inet_ntop (int, const void *, char *, socklen_t);
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

#define BUFFERSIZE 32

int main()
{
    char buffer[BUFFERSIZE];
    fgets(buffer, 31, stdin);
    buffer[31] = '\0';

    struct in_addr in;
    in.s_addr = 0;

    if(inet_aton(buffer, &in) == 0) {
        printf("invalid address\n");
    } else {
        printf("%#x\n",in.s_addr);
    }
    /*
        172.17.242.131
        0x83f211ac
        ---
        255.255.255.255
        0xffffffff
    */
    return 0;
}

(3)getsockopt()和setscokopt()

对套接字的工作方式有特殊要求时需要修改套接字的属性。Linux系统提供了套接字选项函数来控制套接字的属性。

  • *getsockopt()*获取套接字的属性
  • *setsockopt()*设置套接字的属性
#include <sys/types.h>
#include <sys/socket.h>

int setsockopt (int __s, int __level, int __optname, const void *optval,
		  socklen_t __optlen);
int getsockopt (int __s, int __level, int __optname, void *__optval,
		  socklen_t *__optlen);
  • 第一个参数*__s(int)*是一个套接字。
  • 第二个参数*__level(int)*是进行套接字选项操作的层次
    • SOL_SOCKET:通用套接字
    • IPPROTO_IP:IP层套接字
    • IPPROTO_TCP:TCP层套接字
  • 第三个参数*__optname(int)*是套接字选项的名称
  • getsockopt()
    • *optval(const void *)*存放获得的套接字选项
    • __optlen(socklen_t)
      • 调用前:optval指向的空间的大小
      • 调用后:optval所保存的结果的实际大小
  • stesockopt()
    • *optval(const void *)*是待设置的套接字选项
    • *__optlen(socklen_t)*是该选项的长度
  • 函数返回值:执行成功均返回0;错误均返回-1,并将代码存入errno中。

*(4)多路复用select()

客户端/服务器模型中,服务器端需要同时处理多个客户端的连接请求,此时就需要使用多路复用。实现多路复用最简单的方法是采用非阻塞方式套接字,服务器端不断地查询各个套接字的状态,如果有数据到达则读出数据,如果没有数据则查看下一个套接字。这种方法虽然简单,但轮询过程效率很低

另一种方法是:服务器进程向系统登记希望监视的套接字并阻塞,而不主动询问套接字状态。当套接字上有事件发生时(有数据到达),系统通知服务器进程告知哪个套接字上发生了什么事件,服务器进程查询对应套接字并进行处理。在这种工作方式下,套接字上没有事件时,服务器进程不会去查询套接字的状态,提高了效率。

#include <sys/select.h>

int select __P ((int __n, fd_set *__readfds, fd_set *__writefds,
		 fd_set *__exceptfds, struct timeval *__timeout));
  • 第一个参数*__n(int)是需要监视的文件描述符数,要监视的文件描述符值为0~n-1*。
  • 第二个参数*__readfds(fd_set *)*指定需要监视的可读文件描述符集合
    • 当这个集合中的一个文件描述符上有数据到达时,系统通知调用select函数的程序
  • 第三个参数*__writefds(fd_set *)*指定需要监视的可写文件描述符集合
    • 当这个集合中的某个文件描述符可以发送数据时,程序将收到通知
  • 第四个参数*__exceptfds(fd_set *)*指定需要监视的异常文件描述符集合
    • 当这个集合中的某个文件描述符发生异常时,程序将收到通知
  • 第五个参数*__timeout(struct timeval *)*指定阻塞时间
    • 如果这段时间内监视的文件描述符上都没有事件发送,则函数select将返回0
  • 这里的文件描述符可以是普通文件的描述符,也可以是套接字描述符。
  • 函数返回值:如果select函数设定的要监视的文件描述符集合中有描述符发生了事件,则select函数将返回发生事件的文件描述符的个数。

Linux系统为文件描述符集合提供了一系列的宏以方便操作:

#  define	FD_SET(n, p)	((p)->fds_bits[(n)/NFDBITS] |= (1L << ((n) % NFDBITS)))
#  define	FD_CLR(n, p)	((p)->fds_bits[(n)/NFDBITS] &= ~(1L << ((n) % NFDBITS)))
#  define	FD_ISSET(n, p)	((p)->fds_bits[(n)/NFDBITS] & (1L << ((n) % NFDBITS)))
#  define	FD_ZERO(p)	(__extension__ (void)({ \
     size_t __i; \
     char *__tmp = (char *)p; \
     for (__i = 0; __i < sizeof (*(p)); ++__i) \
       *__tmp++ = 0; \
}))
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值