基本TCP客户服务器模型
IPV4套接字结构
-
struct sockaddr
/* 通用套接字地址 */ #include
struct sockaddr { uint8_t sa_len; /* 1字节 */ sa_family_t sa_family; /* 1字节 */ char s_data[14]; /* 14字节 包含目标地址和端口信息 */ }; -
struct sockaddr_in
/* IPV4地址,网络字节序 */ typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; /* address in network byte order */ }; /* IPV4套接字地址 */ #include
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ uint16_t sin_port; /* port in network byte order 2字节 */ struct in_addr sin_addr; /* internet address 4 字节 */ char sin_zero[8]; };
struct sockaddr 和 struct sockaddr_in 区别和联系
- 2者都是16字节长度
-
connect
等后面系统调用使用的地址是 sockaddr
socket 函数
#include
int socket(int family, int type, int protocol) 成功返回非负描述符,若出错返回-1
family指明协议族:PF_INET
, PF_INET6
, PF_LOCAL
....
type指明套接字类型:SOCK_STREAM
(TCP),SOCK_DGRAM
(UDP),SOCK_RAW
(原始套接字)
protocol指明某个协议类型的常值,现在基本废弃,一般设为0。
socket成功返回套接字描述符。
- 示例:创建一个非阻塞的ipv4套接字
int createSocket() { int ret = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (ret == -1) { SYSFATAL("socket() Error"); } return ret; }
connect 函数
#include
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
成功则为0, 若出错则为-1
connect用来与远程服务器建立连接。若是TCP套接字,调用connect将激发3路握手的过程,仅在连接建立成功或者出错时才返回。
- 若TCP客户没有收到SYN分节的响应,则返回 ETIMEDOUT 错误。
- 若对客户的SYN的响应式RST,则表明服务器主机在我们指定的端口上没有进程在等待与之连接, 这是一种"硬错误",会立即返回 ECONNREFUSED 错误。
产生RST的3个条件:
- 目的地为某端口SYN到达,而该端口没有上没有监听的服务、
- TCP想取消一个已有连接
- TCP接受到一个根本不存在的连接
connect函数会导致当前套接字状态从 CLOSED 状态(该套接字自从由socket函数创建以来一直所处的状态)
转移到SYN_SENT状态,若成功再转移到 ESTABLISHED 状态。每次connect失败后,都必须close当前的套接字描述符并重新调用 socket。
关于阻塞和非阻塞的connect
在 socket 是阻塞模式下 connect 函数会一直到有明确的结果才会返回(或连接成功或连接失败),
在实际项目中,我们一般倾向使用所谓的异步的 connect 技术,或者叫非阻塞的 connect。
bind 函数
#include
int bind(int sockfd, const struct sockaddr *myaddr, socklen_ addrlen);
若成功则为0,若出错则为-1
bind函数把一个本地协议地址赋予一个套接字。
struct sockaddr_in addr_;
addr_.sin_family = AF_INET;
in_addr_t ip = INADDR_ANY // INADDR_LOOPBACK
addr_.sin_port = htons(port);
addr_.sin_addr.s_addr = htonl(ip);
INADDR_ANY
表示地址是 0.0.0.0 (主机序的数值表达形式)
INADDR_LOOPBACK
表示 127.0.0.1 (主机序的数值表达形式)
假设一台机器对外访问的ip地址是120.55.94.78,这台机器在当前局域网的地址是192.168.1.104;同时这台机器有本地回环地址127.0.0.1。
如果你指向本机上可以访问,那么你 bind 函数中的地址就可以使用127.0.0.1 或 INADDR_LOOPBACK
。
如果你的服务只想被局域网内部机器访问,bind 函数的地址可以使用192.168.1.104。
如果 希望这个服务可以被公网访问,你就可以使用地址 0.0.0.0 或 INADDR_ANY
。
服务器在启动时捆绑它总所周知的端口,如果一个TCP客户或者服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口。
-
进程也可以把一个特定的IP地址捆绑到它的套接字上。
- 对于TCP客户,这就为该套接字上发送的IP数据报指派了源IP地址,通常,TCP客户不用设置这一步,因为内核会根据外出网络接口来选择源IP地址。
- 对于TCP服务器,这就限定了该套接字只接受那些目的地为这个IP地址的客户连接,如果TCP服务器没有把IP地址捆绑到它的套接字上,内核就把客户发送SYN的目的IP地址作为服务器的IP地址。
listen函数
#include
int listen(int sockfd, int backlog); 若成功则为0,若出错则为-1
- 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说将调用
connect
发起连接的客户套接字。而listen
函数把一个未连接的套接字转换为一个被动套接字。 - 理解backlog参数
内核为任何一个给定的监听套接字维护2个队列。
- 未完成的连接队列,每一个SYN分节对应其中一项
- 已完成的连接队列,每个已完成TCP三路握手过程的客户。
当来自客户的SYN到达时,TCP在 未完成连接队列 中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,捎带对客户SYN的ACK。
这一项一直保留在未完成连接队列中,直到3路握手的第三个分节(客户对服务器的SYN的ACK)到达或者该项超时。如果三路握手正常完成,该项就从未完成连接队列移到已完成的连接队列队尾。这里的backlog
,在 Linux 中表示已完成 (ESTABLISHED) 且未 accept
的队列大小。
accept 函数
#include
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
成功返回非负描述符, 若出错返回-1
accept函数由TCP服务器调用,用于从已完成连接队列头部返回一个已经完成连接的客户,如果已完成连接队列为空,则该进程会被投入睡眠。成功,返回值是一个由内核产生的全新描述符,代表与客户的连接,称之为已连接套接字。
close函数
#include
int close(int sockfd); 返回:若成功则为0,如出错则为-1
close这个函数会对套接字引用计数减1,一旦发现套接字引用计数到 0,就会对套接字进行彻底释放,并且会关闭 TCP 两个方向的数据流。
在输入方向,系统内核会将该套接字设置为不可读,任何读操作都会返回异常。
在输出方向,系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文,接下来如果再对该套接字进行写操作会返回异常。
shutdown函数
#include
int shutdown(int sockfd, int howto); 返回值:成功为0,出错则为-1
函数行为依赖于howto参数的值。
- SHUT_RD:关闭连接的读的一半
套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。 - SHUT_WR:关闭连接的写这一半--这称为半关闭,
当前留在套接字发送缓冲区的数据将被发送掉,然后TCP正常连接终止序列。 - SHUT_RDWR:连接的读半部和写半部都关闭。
shutdown函数和close函数相比,close有2个限制:
- close函数把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。shutdown函数不管引用计数都会激发TCP的正常连接终止序列。
- close终止读和写2个方向的数据传送。
inet_pton和inet_ntop函数
#include
int inet_pton(int family, const char* strptr, void *addrptr);
返回值:若成功则为1,若输入不是有效的表达格式则为0,若出错则为1,且errno置为EAFNOSUPPORT
const char* inet_ntop(int family, const void* addrptr, char *strptr, size_t len);
返回值:若成功则为指向结果的指针,若出错则为NULL,且errno置为EAFNOSUPPORT。
eg: inet_pton(AF_INET, ip_addr, &(cliname.sin_addr));
eg: inet_ntop(AF_INET, (&servaddr.sin_addr), buf, 64);
inet_pton负责将字符串转为数值格式。
inet_ntop负责将数值转为字符串格式。
p(presentation)和数值n(numeric)。
htons和htonl函数
#include
uint16_t htons(uint16_t hostbit16value);
uint32_t htonl(uint32_t host32bitvalue);
均返回,网络字节序的值。
将主机序转为网络序(大端字节序)。
举例:时间服务器
客户
客户端通过创建 socket,connect 发起连接建立请求。
int main(int argc, char** argv)
{
int sockfd, n;
struct sockaddr_in servaddr;
char buf[MAXLINE];
if (argc != 2)
err_quit("usage ");
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_sys("socket error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
err_sys("inet_pton error");
if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) < 0)
err_sys("connect error");
while( (n = read(sockfd, buf, MAXLINE)) > 0) {
buf[MAXLINE] = '\0';
if (fputs(buf, stdout) == EOF)
err_sys("fpus error");
}
if (n < 0)
err_sys("read error");
}
服务器
服务器端通过创建 socket,bind,listen 完成初始化,通过 accept 完成连接的建立。
int main(int argc, char**argv)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buf[MAXLINE];
time_t ticks;
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_sys("sockfd error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
while(1) {
connfd = accept(listenfd, NULL, NULL);
ticks = time(NULL);
snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
write(connfd, buf, strlen(buf));
close(connfd);
}
}
参考资料
1、《UNIX 网络编程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff