10-socket
socket
创建socket时先给进程分配一个类似文件描述符的资源,不能进程间共享。接下来会给套接字起名,本地套接字的名字时Linux文件系统中的文件名,一般在 /usr 或 /usr/tmp 目录中。它的名字是与客户连接的特定服务标识符,端口号或访问点。我们用bind 给套接字命名,然后等待客户端连接到命名套接字。调用 listen 创建一个队列并将其存放来自客户的进入连接。服务器通过 accept 接受客户的连接。调用accept 会创建一个与原有命名套接字不同的新套接字,其只用于与特定客户端的通信,而命名套接字用于处理来自其他客户端的连接。客户端则调用socket 创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用 connect 建立连接。
简单 server
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int server_sockfd, client_sockfd;
int client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;
// 删除并创建新套接字
unlink("server_socket");
server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
// 命名套接字
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, "server_socket");
bind(server_sockfd, (struct sockaddr*)&server_address, sizeof(server_address));
// 建立连接队列
listen(server_sockfd, 5);
while (1)
{
char ch;
printf("server waiting\n");
// 接受一个连接
client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_address, &client_len);
// 操作 socket
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
return 0;
}
简单 client
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int sockfd;
struct sockaddr_un address;
int res;
char ch = 'A';
// 创建套接字
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
// 根据服务器情况给套接字命名
address.sun_family = AF_UNIX;
strcpy(address.sun_path, "server_socket");
// 将客户端连接到服务器的套接字
res = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
if (res == -1) {
printf("opps: client");
exit(1);
}
// 操作 socket
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf("char from server: %c\n", ch);
close(sockfd);
return 0;
}
套接字属性
- 域 domain / 协议族 protocol family
通信中的网络介质。最常见的套接字域就是 AF_INET 指Internet网络,底层的IP协议只有一个地址族。Ipv6对应套接字域AF_INET6。AF_UNIX 底层是文件输入/输出,地址就是文件名。 - 类型 type
每个套接字域可能有多种不同通信方式,AF_INET 有两种机制 SOCK_STREAM 与 SOCK_DGRAM。 - 协议 protocol
当底层传输机制允许不止一个协议来提供要求的套接字类型,可为套接字选一个特定协议。在UNIX网络与文件系统套接字中使用默认值即可。
创建套接字
#include <sys/types.h>
#include <sys/socks.h>
int socket (int __domain, // 一般使用本地套接字 AF_UNIX 或网络套接字 AF_INET
int __type, // tcp的 SOCK_STREAM 和udp的 SOCK_DGRAM
int __protocol); // 一般为 0
domain:
type:
protocol:
通常为0,表示给定域和套接字类型选择默认协议,当同一域和套接字类型支持多个协议时,可用protocol指定
AF_INET 域中,套接字类型 SOCK_STREAM 默认协议为 TCP
AF_INET 域中,套接字类型 SOCK_DGRAM 默认协议为 UDP
关闭套接字
使用close或shutdown
为什么有了close还有shutdown?
- 只有最后一个活动进程关闭时,close才释放网络端点。这意味着使用dup复制套接字要直到关闭了最后一个引用它的文件描述符才会释放这个socket。而shutdown可以使一个socket不处于活动状态,与引用它的文件描述符数无关。
- shutdown 可以只关闭读写的一个方向。如只关闭该socket的写端但仍能读。
套接字地址
对于AF_UNIX域套接字地址
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sun_family; 指定地址类型
char sun_path[]; 套接字地址
};
AF_INET 域中
#include <netinet/in.h>
struct sockaddr_in {
short int sin_family; // 域
unsigned short int sin_port; // 端口号
struct in_addr sin_addr; // ip 地址
}
struct in_addr {
unsigned long int s_addr; // IPv4 地址
}
命名套接字
通过给套接字命名,AF_UNIX 关联到文件系统路径名,AF_INET 关联到一个IP端口号。
#include <sys/socket.h>
int bind(int socket,
const struct sockaddr *address,
size_t address_len);
// 发现绑定到套接字上的地址
int getsockname (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __len);
// 连接后找到对方地址
int getpeername (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __len);
#include <netinet/in.h>
INADDR_ANY // 绑定到所有系统网络接口上
bind 成功时返回0,失败时返回 -1并设置errno
- EBAF fd无效
- ENOTSOCK 对应的不是一个套接字
- EINVAL 对应的是一个已命名的套接字
- EADDRNOTAVAIL 地址不可用
- EADDRINUSE 地址已绑定一个套接字
创建套接字队列
#include <sys/socket.h>
int listen(int socket,
int backlog); // 队列最大长度
#include <sys/socket.h>
SOMAXCONN // 队列最大长度
接受连接
#include <sys/socket.h>
/*
事先必须先由bind调用命名,并由listen为其分配连接队列。
*/
// 返回新的套接字描述符,新套接字类型和服务器监听套接字类型相同
int accept(int socket,
struct sockaddr *address, // 客户的地址放入sockaddr结构中,可设为空
size_t *address_len); // 指定客户结构长度,超过将被截断;返回时被设为地址结构实际长度
若没有未处理的连接,accept将阻塞。通过套接字描述符设置 O_NONBLOCK 标志改变这一行为
int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, ONONBLOCK|flags);
请求连接
#include <sys/socket.h>
/*
阻塞一段不确定超时时间。如果被一个信号中断调用会失败,并以异步方式继续建立。也可通过文件描述符的 O_NONBLOCK 标志改变
*/
int connect(int socket,
const struct sockaddr *address,
size_t address_len); // 指定address参数的长度
关闭套接字
使用 close 函数关闭。如果时面向连接型的并设置了 SOCK_LINGER 选项,close调用会在该套接字还有未传输数据时阻塞。
数据传输
套接字端点表示为一个文件描述符
3个发送数据的函数(send sendto sendmsg),3个接收数据的函数(recv recvfrom recvmsg)。
send用于tcp
sendto 用于 udp
sendmsg
recv
recvfrom
recvmsg
套接字选项
字节序
通过netstat命令看到的端口与指定的数字不同,因为通过套接字端口传递的端口号和地址都是二进制数字。不同计算机使用不同字节序。如:Intel使用大端存储,IBM PowerPC使用小端存储。如果逐位复制内存中整数两个计算机的整数值不一致。为此需要使用网络字节序。
TCP/IP 协议栈使用大端字节序。
#include <netinet/in.h>
// 将16位与32位整数在主机字节序与网络字节序间转换。
// n表示网络,h表示主机,l表示长,s表示短
unsigned long int htonl(unsigned long int hostlong); // host to network, long
unsigned short int htons(unsigned short int hostshort); // host to network, short
unsigned long int ntohl(unsigned long int netlong); // network to host, long
unsigned short int ntohs(unsigned short int netshort); // network to host, short
用以上函数处理端口
网络信息
可以将服务与端口添加到 /etc/services
,将主机添加到/etc/hosts
。
#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr, size_t len, int type);
struct hostent *gethostbyname(const char *name);
struct hostent
{
char *h_name; /* Official name of host. */
char **h_aliases; /* Alias list. */
int h_addrtype; /* Host address type. */
int h_length; /* Length of address. */
char **h_addr_list; /* List of addresses from name server. */
};
struct servent *getserverbyname(const char *name, const char *proto);
struct servent *getserverbyport(int port, const char *proto); // 第二个参数位 SOCK_STREAM SOCK_DGRAM 等
struct servent
{
char *s_name; /* Official service name. */
char **s_aliases; /* Alias list. */
int s_port; /* Port number. */
char *s_proto; /* Protocol to use. */
};
#include <arpa/inet.h>
/*要把返回的地址列表转换为正确的地址类型,并用函数 inet_ntoa 将它们从网络字节序转换为可打印字符。
将一个因特网主机地址转换为一个点分四元组格式的字符串。
*/
char *inet_ntoa(struct in_addr in);
// 将网络字节序的二进制地址转换成文本字符串格式,第一个参数仅支持 AF_INET AF_INET6
// __len 常用 INET_ADDRSTRLEN
const char *inet_ntop (int __af, const void *__restrict __cp,
char *__restrict __buf, socklen_t __len);
// __len 常用 INET6_ADDRSTRLEN
// 将文本字符串格式转换成网络字节序二进制地址
int inet_pton (int __af, const char *__restrict __cp,
void *__restrict __buf);
因特网守护进程(xinetd / inetd)
UNIX 系统通常以超级服务器的方式提供多项网络服务,其通常监听许多端口地址上的连接。当由客户连接到某项服务时,守护进程就运行相应的服务器。这使得针对各项网络服务的服务器不用一直运行,在需要时启动。通常使用图形化界面管理,或/etc/xinetd.conf
和/etc/xinetd.d
目录下的文件。
套接字选项
#include <sys/socket.h>
int setsockopt(int socket,
int level, // 针对不同套接字级别设置,SOL_SOCKET套接字级别,IPPORTO_TCP对应tcp级别
int option_name, //设置选项
const void *option_value,
size_t option_len); // option_value的长度
多路复用 select
select 直接操纵多个文件描述符的集合 fd_set
#include <sys/types.h>
#include <sys/time.h>
// 初始化空集合
void FD_ZERO(fd_set *fdset);
// 从集合中清除fd
void FD_CLR(int fd, fd_set *fdset);
// 添加fd到集合
void FD_SET(int fd, fd_set *fdset);
// 判断是否在set中,在 非零值,不在 零
int FD_ISSET(int fd, fd_set *fdset);
// 超时值
struct timeval {
time_t tv_sec; // seconds
long tv_usec; // microseconds
}
/*
用于测试fd_set中是否由fd处于可读或可写或错误状态
当__readfds中有可读fd,__writefds中有科协fd,__exceptfds中有错误fd
成功返回状态发生变化的fd总数,失败返回-1并设置errno
*/
extern int select (int __nfds, // 需要测试的fd数目
fd_set * __readfds,
fd_set * __writefds,
fd_set * __exceptfds,
struct timeval * __timeout); // Linux在退出时会将此选项清空,故每次进入select前重新设置此选项
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
int result;
fd_set readfds, testfds;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
listen(server_sockfd, 5);
FD_ZERO(&readfds);
FD_SET(server_address, &readfds);
while (1) {
char ch;
int fd;
int nread;
testfds = readfds;
printf("server waiting\n");
result = select(FD_SETSIZE, &testfds, NULL, NULL, 0);
if (result < 1) {
perror("select failed");
exit(1);
}
for (fd = 0; fd < FD_SETSIZE; fd++) {
if (FD_ISSET(fd, &testfds)) {
if (fd == server_sockfd) {
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr*) &client_address,
&client_len);
FD_SET(client_sockfd, &readfds);
printf("adding client on fd %d\n", client_sockfd);
} else {
ioctl(fd, FIONREAD, &nread);
if (nread == 0) {
close(fd);
FD_CLR(fd, &readfds);
printf("removing client on fd %d\n", fd);
} else {
read(fd, &ch, 1);
sleep(5);
printf("serving client on fd %d\n", fd);
ch++;
write(fd, &ch, 1);
}
}
}
}
}
return 0;
}
UDP
ssize_t sendto (int __fd,
const void *__buf, size_t __n,
int __flags,
__CONST_SOCKADDR_ARG __addr,
socklen_t __addr_len);
ssize_t recvfrom (int __fd,
void *__restrict __buf,
size_t __n,
int __flags,
__SOCKADDR_ARG __addr,
socklen_t * __addr_len);