文章目录
前言
因为socket编程中涉及到的结构比较多,因此在此做了一些记录,方便后续的查阅。
1. 结构体相关
1)sockaddr和sockaddr_in详解
sockaddr
在很早的版本使用,其中成员sa_data
把通信的IP地址和端口混合在一起,结构体和头文件见如下:
#include <sys/socket.h>
struct sockaddr {
sa_family_t sin_family;
char sa_data[14];
}
sockaddr_in
将早期的结构的ip和端口分离出来,结构如下:
#include <netinet/in.h>
#include <arpa/inet.h>
struct sockaddr_in
{
sa_family_t sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr
{
in_addr_t s_addr;
};
sin_port和sin_addr都必须是网络字节序(大端字节序),而我们通常使用的linux等是小端字节序(主机字节序),因此需要使用接口转换。
2) 字节序转换的函数
~~inet_addr
~~如今已废除,参考<<UNIX网络编程卷1>>3.6节P68,替代的有inet_addr,更好的替代是inet_pton(支持IPv4和IPv6)
#include <arpa/inet.h>
/*返回:若字符串有效则为1,否则为0/*/
int inet_aton(const char *strptr, struct in_addr *addrptr);
/*返回:若字符串有效则为32位二进制网络字节序的IPv4地址,否则为INADDR_NONE*/
in_addr_t inet_addr(const char *strptr);
/*返回:指向一个点分十进制数串的指针*/
char *inet_ntoa(struct in_addr inaddr);
/*返回:若成功则为1,若输入不是有效的表达式则为0, 若出错则为-1*/
int inet_pton(int family, const char *strptr, void *addrptr/*&foo.sin_addr*/);
/*如成功则为指向结果的指针,若出错则为NULL*/
len:如果是IPv4,填写16,在<netinet/in.h>中有定义宏
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
char str[INET_ADDRSTRLEN];
ptr = inet_ntop(AF_INET, &foo.sin_addr, str, sizeof(str));
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
e.g:
struct sockaddr_in servaddr;
servaddr.sin_port = htons(13);
servaddr.sin_addr.s_addr = inet_addr("192.168.1.1");
//尽量使用如下,因为该接口不仅支持IPV4还支持IPV6
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst); //将"点分十进制"->"二进制整数”
//struct in_addr s;
char *ip = "192.168.1.1";
inet_pton(AF_INET, (void*) ip, (void*)&s);
注意: 如果端口是0
()或INADDR_ANY
(值通常为0), 可以不转为网络字节序(NBO),因为使用htonl并非必要,不过头文件<netinet/in.h>中定义了所有INADDR_按照主机字节序定义,我们还是要习惯使用htonl转<<UNIX网络编程卷1>>4.4节P83。
推荐使用inet_pton
,支持IPV4和IPV6,只做小小的改动,但仅修改一点点,
其他地方都需要修改,因此更好的做法是编写协议无关
的程序(11-11,使用了getaddrinfo函数)
struct sockaddr_in ->struct sockaddr_in6
socket(AF_INET, SOCK_STREAM,0) -> socket(AF_INET6, SOCK_STREAM, 0);
servaddr.sin_family = AF_INET -> servaddr.sin6_family = AF_INET6;
servaddr.sin_port -> servaddr_sin6_port
inet_pton(AF_INET,...,...) -> inet_pton(AF_INET6,...,...);
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd, n, counter = 0;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: a.out <IPaddress>");
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_sys("socket error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET; /*协议族成员设置为AF_INET*/
servaddr.sin_port = htons(13); /* daytime server */
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
err_quit("inet_pton error for %s", argv[1]);
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
err_sys("connect error");
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
counter++;
recvline[n] = 0; /* null terminate */
if (fputs(recvline, stdout) == EOF)
err_sys("fputs error");
}
if (n < 0)
err_sys("read error");
printf("counter = %d\n", counter);
exit(0);
}
3)问题和结论
3-1)编写协议无关的代码,支持IPv4和IPv6
3-2) 使用域名替代点分十进制的方式
2. API相关
1)connect函数
#include <sys/socket.h>
/*返回: 若成功则为0,若出错则为-1*/
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
如果是TCP套接字,connect激发TCP的三次握手过程,仅再链接成功或出错返回
,出错有如下几种情况:
- 客户端发送SYN分节,没有收到服务端的响应,返回ETIMEDOUT错误,后续会间隔时间继续发送SYN。
- 若对客户的SYN响应是
RST
(表示复位),表明服务器在客户端指定的端口无进程运行,这是一种硬错,客户一接收到RST就马上返回ECONNREFUSED错误。
客户端运行,服务端不运行
root@ubuntu:/opt/socket/unpv13e/intro# ./daytimetcpcli 127.0.0.1
connect error: Connection refused
抓包:
- 若客户发出SYN分节路由器转发ICMP错误, 认为是软错误(soft error),客户主机内核保存该消息,按照时间间隔继续发送,如果再持续一段时间(75s)任然没有收到响应, ICMP错误(EHOSTUNREACH或ENETUNREACH)返回给客户端。
2)bind函数
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr * myaddr, socklen_t addrlen);//返回0成功,-1出错
- 客户端一般不调用bing,即
使用通配地址(INADDR_ANY)
和端口0(内核自行分配端口)
IP地址和端口内核选择; - 服务端一般会指定端口, IPv4地址使用
通配地址(INADDR_ANY,其值一般为0)
,如果服务器没有把IP地址绑到它的套接字上,内核就把客户发送的SYN的目的IP地址作为服务器的源IP地址,如果是IPv6,参考4.4节的方式处理。
-
如果让内核来为套接字选择临时端口,想获取分配的端口,需要调用getsockname来返回协议地址。通常指定服务器的端口应该在(5000, 49152)范围,避免与临时端口号(一般大于49152或小于5000 P98详)
-
从bind函数返回的一个常见错误是EADDRINUSE(地址已被使用)。解决方式使用套接字选项SO_REUSEADDR/SO_REUSEPORT来解决,参考7.5节讨论。
后续开专题记录,这里仅做一个目录性的引导,后续在此链接。
3)listen函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);//成功返回0, 出错返回-1
4)accept函数
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);//成功非负描述符,出错-1
3. 难点
后续开专题记录,这里仅做后续的链接。
未完待续…