字节序点这里
TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用的socket地址结构体,分别用于IPv4和IPv6。
struct sockaddr_in
{
sa_family_t sin_family; //地址族:AF_INET
u_int16_t sin_port; //端口号,要用网络字节序表示
struct in_addr sin_addr; //IPv4地址结构体 见下面
};
struct in_addr
{
u_int32_t s_addr; //IPv4地址,要用网络字节序表示
};
struct sockaddr_in6
{
sa_family_t sin6_family; //地址族:AF_INET6
u_int16_t sin6_port; //端口号,要用网络字节序表示
u_int32_t sin6_flowinfo; //流信息,应设置为0
struct in6_addr sin6_addr; //IPv6地址结构体 见下面
u_int32_t sin6_scope_id; //scope ID,尚处于实验阶段
};
struct in6_addr
{
usigned char sa_addr[16]; //IPv6地址,要用网络字节序表示
};
#include
int inet_pton(int af, const char* src, void* dst);
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
inet_pton函数将用字符串表示的IP地址src转换成用网络字节序整数表示的IP地址,并将结果存于dst指向的内存中。成功返回1,失败返回0。
af参数指定地址族,可以是AF_INET或AF_INET6。
inet_ntop进行相反的转换,前三个参数与inet_pton参数相同,最后一个cnt指定目标存储单元的大小。下面的两个宏能帮我们指定这个大小:
#include
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
inet_ntop成功时返回目标存储单元的地址,失败返回NULL,并设置errno。
#include
#include
int socket(int domain, int type, int protocol);
domain指定要使用的底层协议族,对于TCP/IP协议族而言,为PF_INET(Protocol Family of Internet)或PF_INET6,对于UNIX本地域协议族为PF_UNIX。
type指定服务类型,主要是SOCK_STREAM(流服务,TCP),SOCK_UGRAM(数据报,UDP)。自Linux内核版本2.6.17起,type参数可以接受上述服务类型与SOCK_NONBLOCK和SOCK_CLOEXEC相与的值,它们分别表示将新创建的socket设为非阻塞的,以及用fork创建子进程时在子进程中关闭该socket。
protocol表示一个具体的协议,但是通常前两个参数就唯一确定了第三个值,所以一般设为0表示使用默认协议。
socket系统调用成功返回一个socket文件描述符,失败返回-1并设置errno。
#include
#include
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
bind将addr指定的地址绑定到sockfd上,addrlen为sizeof(addr)。对于TCP字节流(UDP数据报)通信,socket类型是AF_INET和SOCK_STREAM(SOCK_DGRAM),socket上绑定的是IP、Port等信息,对于本地进程间通信,socket类型是AF_LOCAL和SOCK_STREAM(SOCK_DGRAM),socket上绑定的是一个本地文件,这也是本地socket和网络socket之间的最大区别。
另外AF_UNIX也属于本地socket,与AF_LOCAL是等价的。
要注意的是sockaddr是旧的地址结构,从新的结构到旧的结构直接强制转换就可以了,看后面的代码就清楚了。
bind成功返回0,失败返回-1,并设置errno。
#include
int listen(int sockfd, int backlog);
sockfd指定被监听的socket。(面试预警)backlog参数提示内核监听队列的最大长度,即accept队列长度。监听队列的长度如果超过backlog,服务器将不受理新的客户端连接,客户端也将收到ECONNREFUSED错误信息。在内核版本2.2前,backlog指所有处于半连接状态(SYN_RCVD)和全连接状态的socket(ESTABLISHED)的上限。2.2之后只表示全连接队列上限,半连接队列上限由/proc/sys/net/ipv4/tcp_max_syn_backlog定义,典型值是5。但是我的默认值是256。
#include
#include
int accept(int sockfd, struct sockaddr *addr, socklen_t* addrlen);
addr用来获取被接受连接的远端socket地址,长度由addrlen指定。
accept成功时返回一个新的连接socket文件描述符,失败时返回-1,设置errno。
accept函数是阻塞的。
(面试预警)处于ESTABLISHED状态的连接在被accept前,如果出现异常,如掉线(仍处于ESTABLISHED)或提前退出(处于CLOSE_WAIT),accpet是否能正常返回。答案为是的,能正确返回,因为accpet只负责从对队列中取出连接,而不检查连接状态,更不关心网络状况的变化。
由上面的CLOSE_WAIT想到另外一个问题,即处于accept队列里的连接在收到客户端的断开请求后只是回了个ACK,却并不能发送自己的FIN,因此会一直处于CLOSE_WAIT即半关闭状态,直到它被accept后调用close或着程序退出。下面是我的测试代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
if(argc <= 2)
{
printf("usage:%s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, 5);
assert(ret != -1);
//暂停20s以等待客户端连接和相关操作(掉线或退出)
sleep(20);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if (connfd < 0)
{
printf("errno is : %d\n", errno);
}
else
{
//接收连接成功 打印客户端ip和port
char remote[INET_ADDRSTRLEN];
printf("connected with ip: %s and port: %d \n", inet_ntop(AF_INET,
&client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));
// close(connfd);
}
close(sock);
while(1)
{
printf("loop...\n");
sleep(2);
}
return 0;
}
下面是tcpdump输出,telnet退出后,状态变为CLOSE_WAIT,我始终没有调用close,直到最后程序退出,状态又变为LAST_ACK
服务端listen之后(注意:不论是否accept了),客户端就可以调用connect发起连接。
#include
#include
int connect(int sockfd, const struct sockaddr* server_addr, socklen_t addrlen);
sockfd是由socket系统调用的返回值,server_addr是需要连接的服务端的socket地址,addrlen是长度。
connect成功返回一个socket,失败返回-1,并设置errno。
这里又想到一个问题:既然服务端只要listen之后,客户端就可以连接成功,那么在没有accpet的情况下,客户端发送数据 服务端是否能收到呢?验证一下!
test_server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_SIZE 512
int main(int argc, char* argv[])
{
const char* ip = "127.0.0.1";
int port = 12345;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, 5);
assert(ret != -1);
//暂停20s以等待客户端连接和相关操作(掉线或退出)
printf("start sleep!\n");
sleep(20);
printf("wakeup!\n");
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if (connfd < 0)
{
printf("errno is : %d\n", errno);
}
else
{
//接收连接成功 打印客户端ip和port
char remote[INET_ADDRSTRLEN];
printf("connected with ip: %s and port: %d \n", inet_ntop(AF_INET,
&client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));
//尝试读取数据
char buffer[BUFFER_SIZE];
memset(buffer, '\0', BUFFER_SIZE);
while (recv(connfd, buffer, BUFFER_SIZE-1, 0))
{
printf("recv from client %s\n", buffer);
}
close(connfd);
}
close(sock);
return 0;
}
test_client.c
#include "header.h"
#define BUFFER_SIZE 10
int main(int argc, char* argv[])
{
const char* ip ="127.0.0.1";
int port = 12345;
struct sockaddr_in server_address;
bzero(&server_address, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_address.sin_addr);
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
//主动连接服务器 并发送数据
if (connect(sock, (struct sockaddr*)&server_address, sizeof(server_address)) != -1)
{
char buffer[BUFFER_SIZE];
memset(&buffer, 'a', BUFFER_SIZE);
send(sock, buffer, BUFFER_SIZE, 0);
printf("client send complete!\n");
}
else
{
printf("connect error %d\n", errno);
}
close(sock);
return 0;
}
下图是实验结果,注意tcpdump部分红框部分,前一个包是客户端发完数据调用close向服务端发送了FIN后,服务端回复的ACK,后一个包是3s后服务端sleep结束,accept了这条连接后,读出数据,然后调用close给客户端发送FIN。实验表明,虽未被accept,但是处于ESTABLISHED状态的连接已是一条完整的连接,它有自己的接收缓冲区,所以是可以接受发来的数据的,只是接收缓冲区的数据不能被读取而已。
关闭一个连接就是关闭连接对应的socket,通常是通过调用关闭文件描述符的系统调用close来完成。
#include
int close(int fd);
值得注意的是close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1,只有当fd的引用计数为0,才能真正关闭连接。多进程程序中,一次fork默认将使父进程中打开的socket的引用计数加1,因此必须在父进程和紫禁城中都对该socket执行close才能将连接关闭。
如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以用shutdown,与close相比,shutdown被称为优雅的关闭连接,因为它可以通过参数来控制关闭方式,见下面,而close是关闭读写,shutdown是专门为网络编程设计的。
#include
int shutdown(int fd, int howto);
howto参数决定了shutdown的行为:
Linux中一切皆文件,tcp连接当然也就可以通过使用对文件的读写操作read和write来读写socket数据。另外socket编程接口提供了几个专门用于socket数据读写的系统调用,增加了对数据读写时的控制。
#include
#include
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
recv读取sockfd上的数据,存入buf指定的缓冲区,len是buf大小,flags参数见后面,通常设置为0。recv成功时返回读到的数据长度,返回0表示对方关闭了连接,出错返回-1,并设置errno。
send往sockfd写数据,数据来源于buf,len是buf大小,成功返回实际send的数据长度,失败返回0并设置errno。书上没说在对方关闭连接后send会返回啥(-1?),可以写代码测试下。
下图是flags参数的可选值(来自《Linux高性能服务器编程》)
#include
#include
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd,const void* buf, size_t len, int flags,const struct sockaddr* dest_addr, socklen_t* addrlen);
前四个参数与TCP读写接口recv和send相同,由于udp不是面向连接的,所以src_addr用于指定通信对端的地址。
值得一提的是,recvfrom和sendto同样可以用于tcp连接的读写,只需将后面的地址置位NULL。
可用于TCP流数据和UDP数据报
#include
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
下图是msghdr结构体的定义
msg_name成员指向一个socket地址结构变量,对于TCP,该成员没有意义,必须被设为NULL。
iovec*结构体定义如下:
iov_base指向一块内存的起始地址,iov_len是其长度。msg_iovlen指定这样的iovec结构对象有多少个。
对于recvmsg,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度由msg_iov指向的数组指定,这成为分散读(scatter read),对于sendmsg,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。
msg_control和msg_controllen成员用于辅助数据的传送。
msg_flags将会复制recvmsg/sendmsg中的flags,这些flags的含义与send/recv中的相同。
下面两个系统调用专门用来读取和设置socket文件描述符属性:
#include
int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);
level指定要操作哪个协议的选项(即属性),如IPv4、IPv6、TCP等,option_name指定选项名字,option_value和option_len参数分别被操作选项的值和长度。两个函数都是成功时返回0,失败返回-1并设置errno。下图是socket通信中几个比较常用的socket选项。(图片来自《Linux高性能服务器编程》)
面试预警:SO_REUSEADDR和TCPNODELAY选项。
SO_REUSEADDR使处于TIME_WAIT状态的连接的socket地址可以被使用。
TCP_NODELAY用来禁用Nagle算法。Nagle算法规定了在任意时刻,最多只能有一个未被确认的小段。小段是指数据长度小于MSS的数据块。如现在需要发送两个包A和B,A和B都小于MSS,先发送A,在nagle算法下,在未收到A的ACK前(此时A为未被确认的小段),不能发送B。可以看出Nagle算法虽然减少了网络中包的数量,但是却带来了延迟。一些交互性强的应用(如网络游戏)不允许这种事情发生,可以通过设置TCP_NODELAY选项来禁用Nagle算法。
带外标记:sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否使带外数据
#include
int sockatmart(int sockfd);
地址信息函数:getsockname和getpeername分别用于获取本端与对端socket地址
#include
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
获取主机信息
#include
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
获取服务信息
#include
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
上面获取主机的两个函数与获取服务的两个函数都是不可重入的,即非线程安全的。netdb.h里也给出了可重入的版本,在原函数名后面加上_r即可。
getaddrinfo既能通过主机名获取IP地址(内部使用gethostbyname)也能通过服务名获取端口号(内部使用getservbyname),它是否可重入取决于内部调用的版本
#include
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用gethostbyaddr)和服务名(内部使用getservbyport),它是否可重入叶取决于内部调用的版本
#include
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);