概述
没有什么说的,就是一个小小系统调用,重点在于理解TCP协议栈。
套接字函数
/*
socket函数:指定进程期望的通信协议类型(包括协议族和套接字类型)。
__domain:协议族。
__type:指定套接字类型。
__protocol:某个协议类型,或为0选择前两个参数组合默认值。
返回套接字描述符sockfd。
*/
int socket (int __domain, int __type, int __protocol);
/*
connect函数:TCP客户与TCP服务器建立连接的函数。
TCP客户通过此函数建立与TCP服务器的连接,三次握手操作。
套接字含有客户机IP和端口号;
客户机不必一定bind绑定本地IP地址和端口,
和LwIP一样,内核会选择默认本地网卡IP以及临时端口。
__fd:socket函数返回的套接字描述符,描叙进程使用何种协议族。
__addr:地址结构指针,指定进程绑定服务器的地址和端口号。
__len:地址结构指针长度。
TCP套接字,connect发出三次握手,只有成功和出错才返回。
错误返回:
1、SYN发送出去没有ACK信号,重复发送三次,还是无,则ETIMEDOUT。
2、收到RST,表明服务器主机在指定端口没有进程运行,ECONNREFUSED。
3、由于路由器收到ICMP错误,指示目的网络不可达到。
*/
int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
/*
bind函数:将本地协议地址(地址和端口号)赋给套接字。
把绑定本地结构地址,对于TCP可以指定一个或者都指定或者全部都不指定。
对于端口:
客户机可以不绑定端口,让内核临时选择端口。
服务器必须绑定端口,因为都是用的众所周知的端口。
对于IP:
IP地址必须是主机网络接口之一。
对于客户机,就是为IP数据报源IP地址,可以不绑定,那么内核自动将外出网络接口来选择源IP。
对于服务器就限定该套接字仅接收源IP地址为这个IP地址的客户连接。
*/
int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
/*
listen函数:将套接字状态从close变成listen并且设定最大连接个数。
仅仅供TCP服务器使用。用队列处理多个连接请求。
n:表示内核应该为响应的套接字排队最大连接数目。内核为每个套接字维护两个队列:
1、未完成连接队列:每个这样的SYN分节对应其中一项,已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程,这些套接字处于SYN_RCVD状态。
2、已完成连接队列:完成之后便成为ESTABLISHED状态队列
*/
int listen (int __fd, int __n);
/*
TCP服务器调用,用于从已完成连接队列队头返回下一个己完成连接,如果已完成连接队列为空,那么进程被投入睡眠(假设套接字设置为阻塞方式)。
__fd:监听套接字。服务器仅仅创建一个监听套接字,在服务器生命期内一直存在。
__addr:返回客户协议地址。
__addr_len:传入结构长度,然后内核进行值-结果处理,传入为结构地址长度,返回为内核填入的信息。
返回:调用成功(也就是三次握手成功)将返回一个由内核生成的一个全新的已连接的套接字描述符,用于操作这个连接成功,通过此已连接套接字完成服务后,就被关闭。
如果对客户协议地址不感兴趣,后面两参数可以置空。
*/
int accept (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __addr_len);
int
main(int argc, char **argv)//服务器代码
{
int listenfd, connfd;
socklen_t len;//用于值-结果处理,所以必须要调用函数创建内存
struct sockaddr_in servaddr , cliaddr;
char buff[MAXLINE];
time_t ticks;
listenfd = socket(AF_INET, SOCK_STREAM, 0);//监听套接字创建
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;//服务器IP地址为网卡IP地址
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//通用IP地址,网卡IP
servaddr.sin_port = htons(4098); /* daytime server */
bind(listenfd, (SA *) &servaddr, sizeof(servaddr));//绑定本机IP地址和端口,设置结构体成员而已
listen(listenfd, LISTENQ);//绑定之后就可以开始监听外部连接了,指定了排队最大客户连接数
for ( ; ; ) {
len = sizeof(cliaddr);//值传入
connfd = accept(listenfd, (SA *) &cliaddr, &len );//进入accept投入睡眠,等待客户机被内核接受,握手完毕accept返回,len结果返回。
//返回连接描述符,通过connfd和客户通信。
printf("connection from %s , port %d\n",inet_ntop(AF_INET , &cliaddr.sin_addr , buff , sizeof(buff)) , ntohs(cliaddr.sin_port));
//地址转换,将数字转换成点分十进制字符串。
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));//这个库函数优点。
Write(connfd, buff, strlen(buff));//发送给客户。
Close(connfd);//4次断开操作
}
}
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SA struct sockaddr
int main(int argc, char **argv)//客户机代码
{
int sockfd, n;
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)//创建一个网际字节流套接字,TCP套接字
err_sys("socket error");
//客户机没有bind地址,直接填写服务器端地址和端口信息即可。内核自动加入源IP地址。
bzero(&servaddr, sizeof(servaddr));//传递地址,将N字节清0
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(4098); /* daytime server */
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)//填入服务器IP地址和端口号13
err_quit("inet_pton error for %s", argv[1]);
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)//TCP握手操作,交换数据
err_sys("connect error");
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {//读取服务器应答消息
recvline[n] = 0; /* null terminate */
if (fputs(recvline, stdout) == EOF)
err_sys("fputs error");
}
if (n < 0)
err_sys("read error");
exit(0);
}
/*
close函数:关闭套接字并终止连接。默认行为是把该套接字标记成己关闭 ,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数。然后TCP将尝试发送己排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。实际上close仅仅将套接字描述符引用计数减1并标记套接字为关闭,并没有激发四次断开操作,只有套接字引用计数为0时候,才会引发TCP四次断开序列。(当然这些都是TCP协议栈做的,不需要用户进程管)。shutdown可以直接将套接字引用计数清0,直接引发断开操作。
*/
int close(int sockfd);
并发服务器
int main(int argc, char **argv)//客户程序
{
int listenfd, connfd;//监听fd和连接fd
struct sockaddr_in servaddr , cliaddr;
socklen_t clilen;
pid_t childpid;
/*×××××××××××××××××××服务器端套路开始××××××××××××××××××××*/
listenfd = Socket(AF_INET, SOCK_STREAM, 0);//监听套接字创建
bzero(&servaddr, sizeof(servaddr));//清0
servaddr.sin_family = AF_INET;//服务器IP地址为网卡IP地址
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(4099);//服务器端必须设置对应端口号
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));//绑定本机IP地址和端口,设置结构体成员而已
Listen(listenfd, LISTENQ);//绑定之后,更改套接字为listen状态并指定了排队最大客户连接数
/********************服务器端套路结束*********************/
for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd , (SA *)&cliaddr , &clilen);//传入长度为了达到值-结果
if((childpid = Fork()) == 0){
Close(listenfd);//文件计数关闭一次
server_echo(connfd);
Close(connfd);
exit(0);
}//child,并行服务器就是不停fork处理
Close(connfd);//等待下一个客户机连接。
}
}
从accep返回,listenfd 和connfd 都被复制了,所以引用计数全部变为2。在客户端,第一步就是关闭其复制的listenfd,将其引用计数减去1,因为只需要一个进程服务器;在服务器端则关闭connfd,将其引用计数减去1,因为只需要新进程处理客户请求。状态变化过程如下图: