- socket() 创建套接字
- bind() 绑定本机地址和端口
- connect() 建立连接
- listen() 设置监听套接字
- accept() 接收TCP连接
- recv(), read(), recvfrom() 数据接收
- send(), write(), sendto() 数据发送
- close(), shutdown() 关闭套接字
bind:
bind函数把一个本地协议地址赋予一个套接字。对于网际网协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数参数:
- sockfdsocket:调用返回的文件描述符
- addr:指向特定于协议的地址结构的指针,包含本机IP地址和端口号
- addrlen:地址结构的长度
返回值:成功 0 出错 -1
bind函数指定要捆绑的IP地址和/或端口号:
进程指定 | 结果 | |
IP地址 | 端口 | |
通配地址 | 0 | 内核选择IP地址和端口 |
通配地址 | 非0 | 内核选择IP地址,进程指定端口 |
本地IP地址 | 0 | 进程指定IP地址,内核选择端口 |
本地IP地址 | 非0 | 进程指定IP地址和端口 |
- 如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。
- 如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址。
struct sockaddr_in addr;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
IPv6:IPv6地址存放在结构中,系统预先分配变量in6addr_any的extern声明并将其初始化为常值IN6ADDR_ANY_INIT(extern const struct in6_addr in6addr_any; /*::*/)
struct sockaddr_in6 addr;
addr.sin_addr = in6addr_any;
typedef struct sockaddr SA;
//定义一个struct sockaddr_in类型的变量并清空
listenfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in myaddr;
bzero(&myaddr, sizeof(myaddr));
//填充地址信息
my_addr.sin_family = PF_INET;
my_addr.sinport = htons(8888);
my_addr.sin_addr.s_addr = inet_addr(“192.168.1.100”);
//将my_addr强制转换为struct sockaddr类型在函数中使用
int status = bind(listenfd, (SA *)&my_addr, sizeof(my_addr));
connect:
TCP客户用connect函数来建立与TCP服务器的连接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
函数参数:
- sockfd socket返回的文件描述符
- servaddr 服务器端的地址信息,端口和IP地址
- addrlen serv_addr的长度
返回值:成功 0 出错 -1
typedef struct sockaddr SA;
int status = connect(sockfd, (SA *)&addr, sizeof(addr));
- connect()是客户端使用的系统调用,客户在调用connect函数前不必非得调用bind函数,如果需要,内核会确定源IP地址,并选择一个临时端口作为源端口。
- 对于TCP套接字,调用connect函数将激发TCP的三路握手过程,仅在连接建立成功或出错时才返回。
- 若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误
- 若对客户的SYN响应是RST,表明服务器主机在我们指定的端口上没有进程在等待与之链接(服务器进程也许没在运行)。这是一种硬错误(hard error),客户一接收到RST就马上返回ECONNREFUSED错误。
- 若客户发出的SYN在中间的某个路由器上引发一个”destination unreachable“ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该ICMP错误消息,并按一定时间间隔继续发送SYN。若在某个规定的时间后仍未收到响应,则把保存的消息作为EHOSTUNREACH或ENETUNREACH错误返回给进程。
执行TCP客户端的通常步骤:创建一个TCP套接字并连接到一个服务器
int tcp_connect(const char *hostname, const char *service);
返回值:成功则返回已连接套接字描述符,出错不返回
int tcp_connect(const char *host, const char *serv)
{
int sockfd, n;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof (struct addrinfo)) ;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if ((n = getaddrinfo (host, serv, &hints, &res)) != 0)
err_quit("tcp_connect error for %s, %s: %s",host, serv, gai_strerror(n)) ;
ressave = res;
do {
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd < 0)
continue; /* ignore this one */
if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
break; /* success */
close (listenfd); /* ignore this one */
} while ( (res = res->ai_next) != NULL);
if (res == NULL) /* errno set from final connect() */
err_sys ("tcp_connect error for %s, %s", host, serv);
freeaddrinfo (ressave);
return (sockfd);
}
listen:
listen函数仅由TCP服务器调用,主要实现两个功能:
- 当socket函数创建一个套接字时,它被设为一个主动套接字(一个将调用connect发起连接的客户套接字)。listen函数把一个未连接的套接字转换成一个被动套接字(监听套接字),指示内核应接受指向该套接字的连接请求。根据TCP状态转换图,调用listen函数导致套接字从CLOSED状态转换到LISTEN状态。
- listen函数规定了内核应该为相应套接字排队的最大连接个数。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
函数参数:
- sockfd监听连接的套接字
- backlog指定了正在等待连接的最大队列长度,它的作用在于处理可能同时出现的几个连接请求,DoS(拒绝服务)攻击即利用这个原理,非法的连接占用了全部的连接数,造成正常的连接请求被拒绝
listen(listenfd,5);
内核为任何一个给定的监听套接字维护两个队列:
未完成连接队列(incomplete connection queue)
每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
已完成连接队列(completed connection queue)
每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。
每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是自动的,无需服务器进程插手。
- 当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应三路握手的第二个分节:服务器的SYN响应,其中捎带对客户SYN的ACK。创建的新项一直保留在未完成连接队列,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列的队头项将返回给进程,如果已完成连接队列为空,进程将被投入睡眠,直到TCP在已完成连接队列中放入一项才唤醒进程。
- backlog * 1.5 = 未完成连接队列的最大长度,通常指定为5的backlog值实际允许最多有8项排队。
- 在三路握手正常完成的前提下(没有丢失分节,没有重传),未完成连接队列中的任何一项在其中的存留时间就是一个RTT
tcp_ listen函数:
执行TCP服务器的通常步骤:创建一个TCP套接字,给它捆绑服务器的众所周知端口,并允许接受外来的连接请求。
执行TCP服务器的通常步骤:创建一个TCP套接字,给它捆绑服务器的众所周知端口,并允许接受外来的连接请求。
int tcp_listen(const char *hostname, const char *service, socklen_t *addrlenp);
返回值:成功则返回已连接套接字描述符,出错不返回
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
int listenfd, n;
const int on = 1;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof (struct addrinfo)) ;
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if ((n = getaddrinfo (host, serv, &hints, &res)) != 0)
err_quit("tcp_listen error for %s, %s: %s",host, serv, gai_strerror(n)) ;
ressave = res;
do {
listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (listenfd < 0)
continue; /* error, try next one */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on) ) ;
if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
break; /* success */
close (listenfd); /* bind error, close and try next one */
} while ( (res = res->ai_next) != NULL);
if (res == NULL) /* errno from final socket () or bind () */
err_sys ("tcp_listen error for %s, %s", host, serv);
listen (listenfd, LISTENQ);
if (addrlenp)
*addrlenp = res->ai_addrlen; /* return size of protocol address */
freeaddrinfo (ressave);
return (listenfd);
}
accept:
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,进程被投入睡眠。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
函数参数:
- sockfd 接收客户连接的socket,即listening socket
- addr 返回已连接的对端进程的协议地址
- *addrlen所引用的整数值 调用前,由cliaddr所指的套接字地址结构的长度;返回时,由内核存放在该套接字地址结构内的确切字节数
accept函数最多返回一下三个值中的其中之一:
- 一个新的已连接套接字描述符
- 出错指示的整数
- 客户进程的协议地址(*cliaddr),该地址的大小(*addrlen)
如果对返回客户协议地址无兴趣,将cliaddr,addrlen均置为空指针。
typedef struct sockaddr SA;
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd,(SA *)&cliaddr, &clilen);
recv/send:
ssize_t recv(int socket, const void *buffer, size_t length, int flags);
函数参数:
- sockfd socket返回的文件描述符
- buffer 接收数据缓冲区的首地址
- length 接收的字节数
- flags 接收方式 通常为0
int status = recv(sockfd, buf, sizeof(buf), 0);
ssize_t send(int socket, const void *buffer, size_t length, int flags);
函数参数:
- sockfd socket返回的文件描述符
- buffer 发送数据缓冲区的首地址
- length 发送的字节数
- flags 发送方式 通常为0
int status = send(sockfd, buf, sizeof(buf), 0);
read/write:
<span style="font-size:14px;">ssize_t read(int fd, const void *buf, size_t count);</span>
读一个字节流套接字,从一个描述符读n字节
ssize_t readn(int fd, const void *buf, size_t nbytes);
ssize_t readn(int fd, void *vptr, size_t n) /* Read "n" bytes from a descriptor. */
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ((nread = read(fd, ptr, nleft)) < 0)
{
if (errno == EINTR)
nread = 0; /* and call read() again */
else
return(-1);
} else if (nread == 0)
break; /* EOF */
nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
}
ssize_t readline(int fd, void *buf, size_t maxlen);
static int read_cnt;
static char *read_ptr;
static char read_buf[MAXLINE];
static ssize_t my_read(int fd, char *ptr) //每次最多读取MAXLINE个字符,调用一次,每次只返回一个字符
{
if (read_cnt <= 0) {
again:
if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) //如果读取成功,返回read_cnt=读取的字符
{
if (errno == EINTR)
goto again;
return(-1);
} else if (read_cnt == 0)
return(0);
read_ptr = read_buf;
}
read_cnt--; //每次递减1,直到<0读完,才执行上面if的命令。
*ptr = *read_ptr++; //每次读取一个字符,转移一个字符
return(1);
}
ssize_t readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if((rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break; /* newline is stored, like fgets() */
} else if (rc == 0) {
*ptr = 0;
return(n - 1); /* EOF, n - 1 bytes were read */
} else
return(-1); /* error, errno set by read() */
}
*ptr = 0; /* null terminate like fgets() */
return(n);
}
// readlinebuf函数能够展露内部缓冲区的状态,便于调用者查看在当前文本行之后是否收到了新的数据
ssize_t readlinebuf(void **vptrptr)
{
if (read_cnt)
*vptrptr = read_ptr;
return(read_cnt);
}
ssize_t write(int fd, const void *buf, size_t count);
写一个字节流套接字,往一个描述符写n字节
ssize_t written(int fd, const void *buf, size_t nbytes);
ssize_t writen(int fd, const void *vptr, size_t n) /* Write "n" bytes to a descriptor. */
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ((nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return(-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return(n);
}
close/shutdown:
close函数用来关闭套接字,并终止TCP连接
int close(int socketfd);
- 关闭双向通信。close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,TCP尝试发送已排队等待发送到对端的任何数据,完毕后发送TCP连接终止序列。
- 在并发服务器中,fork一个子进程会复制父进程在fork之前创建的所有描述符,复制完成后,相应描述符的引用计数会增加1。父进程调用close关闭已连接套接字只会使相应描述符的引用计数减1,一旦描述符的引用计数为0,内核就会关闭该套接字。调用close后套接字的描述符引用计数仍然大于0的话,就不会引发TCP的四分组连接终止序列。
int shutdown(int sockfd, int howto);
使用shutdown可以不管引用计数就激发TCP的正常连接终止序列。TCP连接是双向的(是可读写的),当我们使用close时 ,会把读写通道都关闭,有时我们希望只关闭一个方向,这时候我们可以使用shutdown。
针对不同的howto,系统会采取不同的关闭方式
- SHUT_RD howto = 0
- SHUT_WR howto = 1
- howto = 2