(一)套接字地址结构
结构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_addr的sin_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将立即返回-1,errno被设置为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; \
}))