Socket编程
套接字的类型
套接字分为两种类型
- Stream Sockets,流格式,传输使用的是TCP协议
- Datagram Sockets,数据包格式,传输使用的是UDP协议
结构体
在不同类型电脑中,字节的排列顺序是不同的:大端序是高位字节存入低地址,低位字节存储高地址;小端序则相反。网络协议都是通过大端的方式传输数据的。
被套接字用到的各种数据类型
-
socket
描述符:int
类型 -
struct sockaddr
:为许多类型的套接字存储套接字地址信息struct sockaddr { unsigned short sa_family; // 地址家族, 设置成AF_INET char sa_data[14]; };
-
为了处理
struct sockaddr
,程序员创造了一个并列的结构:struct sockaddr_in
,in代表Internet
struct sockaddr_in { short int sin_family; // 通信类型, 设置成AF_INET unsigned short int sin_port; // 2字节,端口 struct in_addr sin_addr; // Internet 地址 unsigned char sin_zero[8]; // 为了使该结构体和sockaddr结构的长度相同 }; struct in_addr { unsigned long s_addr; // 4字节 };
字节序
库中提供了从本机字节序转换到网络字节序的函数
#include <arpa/inet.h>
htons(); // Host to Network Short
htonl(); // Host to Network Long
ntohs(); // Network to Host Short
ntohl(); // Network to Host Long
在数据结构struct sockaddr_in
中,sin_addr
和sin_port
需要转换为网络字节序,而sin_family
不需要,因为sin_family
被内核用来决定数据结构中包含什么类型的地址,只是内核在用,不发送到网络上,所以用本地字节序。
上面的这些函数也适用unsigned
类型的参数
如何处理IP地址
如何使用将IP地址转换为unsigned long
类型存储在sockaddr_in
中呢?可以使用inet_addr()
将IP地址从点分十进制转换成无符号长整型:
#include <arpa/inet.h>
// struct sockaddr_in ina;
ina.sin_addr.s_addr = inet_addr("132.241.5.10");
值得注意的是,如果给出的ip地址不合法,那么inet_addr
将返回-1,而-1转换出的ip地址为广播地址255.255.255.255
,所以在使用之前一定要进行检查。
也可以使用相反的方法将长整型的IP地址转换为点分十进制的字符串:
#include <arpa/inet.h>
printf("%s", inet_ntoa(ina.sin_addr)); // 注意这里传入的是结构体
inet_ntoa()
每次调用返回的是一个指向一个字符的指针,这个指向的内存是由函数管理的,每次调用都会覆盖上次调用存下来的IP地址,所以多次调用需要用strcopy()
函数来存储结果。
socket()
函数
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain应该设置成AF_INET
,和struct sockaddr_in
中一样;参数type告诉内核是SOCK_STREAM
类型还是SOCK_DGRAM
类型;最后把protocol设置成0 ,protocol也可以通过getprotobyname()
返回的结构体中得到,详细信息可以查看man手册。
socket()
返回文件描述符,这个描述符可能在之后的各种系统调用中用到;错误时返回-1,错误类型存储在全局变量errno
中。
bind()
函数
拥有套接字之后,就需要把套接字和机器上的一定的端口关联起来。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd是调用socket()
返回的文件描述符;my_addr是指向struct sockaddr
的指针;addrlen就是sizeof(struct sockaddr)
。
在处理IP地址和端口的时候,有些东西是可以自动处理的
my_addr.sin_port = 0; // 随机选择一个没有使用的端口
my_addr.sin_addr.s_addr = INADDR_ANY; // 使用自己的IP地址, 内核会自动处理成自己的IP地址
connect()
函数
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockadddr *serv_addr, int addrlen);
参数格式和bind()
函数一模一样
在这里并没有绑定本地的端口,仅仅设置了对方的端口,因为在连接对方的时候,本地端口并不重要,内核将去自动选择一个合适的端口号。
listen()
函数
用来等待接入请求,并在后面的accept()
中处理请求
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sock是调用soket()
返回的套接字文件描述符;backlog是允许进入队列的连接数目。
accept()
函数
现在的情景是,有人从很远的地方通过一个你正在监听(listen()
)的端口连接(connect()
)到你的电脑。他的连接将加入到等待接受(accept()
)的队列中。你调用accept()
告诉他有空闲的连接,它将返回一个新的套接字文件描述符。现在你就有了两个套接字,一个还在监听端口,新的准备发送(send()
)和接收(recv()
)数据。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockadddr *addr, int *addrlen);
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#define MYPORT 3490
#define BACKLOG 10
int main(void)
{
int sockfd, newfd;
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;
int sin_size;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(my_addr.sin_zero, sizeof(my_addr.sin_zero));
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
listen(sockfd, BACKLOG);
sin_size = sizeof(struct sockaddr_in);
newfd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
return 0;
}
执行后可以发现程序被阻塞在accept一行,当有另外一个程序执行connect请求连接本地IP的3490端口时,程序就会继续执行。
send()
和recv()
函数
这两个函数适用于流式套接字;对于无连接的数据包套接字应该使用sendto()
和recvfrom()
。
#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags);
sockfd是套接字文件描述符;msg指向要发送数据的指针;len是数据的长度;flags设置成0就可以了,其他的设置选项可以在man中查看。函数返回成功发送的字节数。
#include <sys/types.h>
#include <sys/socket.h>
int recv(int sockfd, void *buf, int len, int flags); // len需要传递buf指向内存的大小
如果recv()
接收到字符串大小超过len,那么就会丢弃多出来的字符串。
先运行服务端再运行客户端就可以在客户端看到服务端发送的消息了。
sendto
和recvfrom
#include <sys/types.h>
#include <sys/socket.h>
int sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, int addrlen);
前四个参数和send的一样,dest_addr是要发送信息的目的信息,addrlen可以设置成sizeof(struct sockaddr)
。
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int sockfd, const void *buf, size_t len, int flags,
struct sockaddr *dest_addr, int* addrlen);
前四个参数和recv的一样,后面两个参数是传出参数,里面记录了消息来源的IP、端口等信息。
为了能够正常收发信息,我们需要先明白服务器和客户端各需要什么信息:服务器要知道客户端的ip和端口信息,服务器自己的IP可以自动获取,端口可以让内核自动选择;客户端要绑定自己的ip和端口,客户端不需要关心具体的某个服务器,只需要关注消息是否发给自己就好了。这样消息就可以正常收发了。
SOCK_DGRAM类型的套接字也可以使用connect()
进行连接,连接后就可以简单的调用send()
和recv()
来收发消息,此时依然使用的是UDP,系统会自动加上目标信息和源信息。
close()
和shutdown()
函数
close可以用来关闭socket文件描述符
#include <unistd.h>
int close(int fd);
它将防止套接字上还有数据的读写。此时在套接字的另一端进行读写都将返回错误信息。如果要在关闭套接字上有更多的控制,可以使用shutdown()
#include <sys/socket.h>
int shutdown(int sockfd, int how);
这里的how有三个取值
SHUT_RD; // 禁止读取
SHUT_WR; // 禁止发送
SHUT_RDWR; // 禁止发送和读取
getpeername()
和gethostname()
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
获取套接字对面的地址信息,addrlen应该初始化成addr的内存大小,在执行完之后addrlen会存放addr真正存放的数据大小。
#include <unistd.h>
int gethostname(char *name, int len);
返回程序所运行的主机名字,然后可以使用gethostbyname()
来获得机器的IP地址
域名服务
DNS代表域名服务(Domain Name Service),功能是把网站地址转换成IP地址。
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
struct hostent {
char *h_name; // 服务器的名字
char **h_aliases; // 服务器替代名字的数组,数组最后一个元素是NULL
int h_addrtype; // 地址类型,AF_INET或者是AF_INET6
int h_lengeh; // 地址长度,单位比特
char **h_addr_list; // 服务器IP地址数组(字节序是网络序,输出需要转换),数组最后一个元素是NULL
};
#define h_addr h_addr_list[0]
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
struct hostent *h;
if (argc != 2) { /* 检查命令行 */
fprintf(stderr,"usage: getip address\n");
exit(1);
}
if ((h=gethostbyname(argv[1])) == NULL) { /* 获得地址信息 */
herror("gethostbyname");
exit(1);
}
printf("Host name : %s\n", h->h_name);
printf("IP Address : %s\n", inet_ntoa(*((struct in_addr *)h->h_addr))); // 函数接收参数类型为结构体
char** p = h->h_aliases;
printf("Host aliases name :\n");
for (int i = 0; p[i] != NULL; ++i) {
printf("%s\n", p[i]);
}
printf("Host address list :\n");
p = h->h_addr_list;
for (int i = 0; p[i] != NULL; ++i) {
printf("%s\n", inet_ntoa(*((struct in_addr *)p[i])));
}
return 0;
}