文章目录
TCP协议
简略讲一下工作方式
1.建立连接需要三次握手
2.终止连接要四次握手
建立连接
第一次握手:client向server发SYN,然后进入SYN_SEND状态
(SYN = synchronous 同步的意思)
第二次握手:sever收到之后,会回一个SYN+ACK给client,然后自己进入SYN_RECV状态(synchronous receive)
第三次握手:client接收到之后,再回一个ACK给server,然后就进入Established状态(ACK = acknowledge (告知已收到))
三次握手完成之后就可以开始传输数据啦。
终止连接
第一次握手:可以是client或者是server任意一方提出,然后就会给另一方发送FIN信号
第二次握手:对方收到FIN之后会回一个ACK给对方
第三次握手:收到ACK的一方会回一个FIN给对方
第四次握手:收到FIN的一方会回一个ACK给对方
然后就终止连接啦
TCP(transmission control protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
具体可以在百度百科查看。
实际写代码上一般都唔会接触到这些内层的步骤,但是知道一下还是有必要的。
相关API
socket
socket叫套接字。在课程学习的时候讲到应用程序跟TCP协议之间就需要用到socket这个API来进行操作。用起来有点像open函数。
DESCRIPTION
socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint. The file descriptor returned by a successful call will be the lowest numbered file descriptor not currently open for the process.
socket会为通信创造一个端点,并且会返回一个文件描述符。
什么样的文件描述符?
refer to that endpoint :引用该端点的文件描述符。
然后就像open函数一样,fd是默认分配最小的。
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
看到socket这个API要传入三个参数,domian、type和protocol
1.domain 域
这个domain参数是用来指定一个通讯域的,在man手册里面可以先到详细。
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK AppleTalk ddp(7)
AF_PACKET Low level packet interface packet(7)
AF_ALG Interface to kernel crypto API
比如用ipv4的,选AF_INET
2.type 用来指定通信semantic(语义)
man手册里面也讲到有好多种选择
注意一下:
SOCK_STREAM 是TCP协议用的
SOCK_DGRAM 是UDP协议的。
3.protocol — 协议
这个要传进去的protocol参数是一个数字编号来的。
man手册里面讲到
Normally only a single protocol exists to support a particular socket type
within a given protocol family, in which case protocol can be specified as 0.
这么说通常我们只需要设置为0就行了,只用一个协议嘛。至于怎么进行深入设置的,可以看protocols或者getprotoent。
bind
bind - 捆绑、约束
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
一个socket创建之后呢,它需要一个地址去指派或者说是挂钩起来的。所以需要用bind来给这个sockfd捆绑一个地址;
const struct sockaddr *addr
是个输入型参数;
socklen_t addrlen
指定地址长度,单位是byte。
struct sockaddr *addr
这个结构体指针是啥呢?
可以在**/usr/include/netinet/in.h**里面找到这两个结构体
struct sockaddr_in
这个应该是IPV4用的
struct sockaddr_in6
这个应该是iIPV6用的
下面是struct sockaddr_in
的详细代码
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. 端口号*/
struct in_addr sin_addr; /* Internet address. 地址*/
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
结构体里面
in_port_t
类型的定义为
/* Type to represent a port. */
typedef uint16_t in_port_t;
struct in_addr
定义为
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
把这些记录下来的原因其实不是要说很仔细的研究,就是为了有根可循。
listen
linsten API用于监测监听即将要到来的连接请求。(翻译得有点不好。)
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
传入的第一个参数sockfd就是通过socket API得到的fd,第二个参数backlog用来规定排队的长度的。
The backlog argument defines the maximum length to which the queue of
pending connections for sockfd may grow.
connect
connect的作用是开始通信。
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
里面的参数上述的一些API都有讲述,就不赘述了。
一些辅助的库函数
在man手册里面直接man inet就有这些相关的库函数
inet_aton、inet_ntoa、inetaddr
inet_aton
inet_aton的作用是把IPv4的数字点标识的地址转变成二进制(不过我们printf出来看的时候用的是%x十六进制打印出来看。)
int inet_aton(const char *cp, struct in_addr *inp);
const char *cp
传进去的就是你的ip地址
stuct in_addr *inp
到时候得到转换后的值就会存在inp结构体指针变量指向的结构体里面
下面是测试代码:
//inet_aton的使用
//在IPv4模式下使用,number-and-dot 转换到 binary 形式
//返回值是非0,如果是0则地址无效。
//转换后的地址是存在inp指针指向的一个结构体里面的。
char cp[50] = IP_ADDR;
struct in_addr inp;
int ret = 0;
ret = inet_aton(cp,&inp);
if(0 == ret)
{
printf("the address is not valid\n");
return -1;
}
printf("the convert value is %x\n",inp.s_addr);
printf("\n↓********test of inet_ntoa**********\n");
printf("the convert value is %s\n",inet_ntoa(inp));
inet_ntoa
作用跟inet_aton相反,倒过来转换的,看inet_aton的测试代码的时候可以看到最后一句我直接用了。
char *inet_ntoa(struct in_addr in);
当然,换个方式也行。
//inet_ntoa
char *c;
c = inet_ntoa(inp);
printf("the value is %s\n",c);
printf("\n↓********test of inet_ntoa**********\n")
inet_addr
原型:
in_addr_t inet_addr(const char *cp);
inet_addr的作用跟inet_aton是一样的,结构也差不多,不过它是直接返回到一个in_addr_t类型里面,在上面讲bind的时候也提到过这是一种自定义的类型
//inet_addr
in_addr_t inetaddr = 0;
inetaddr = inet_addr(cp);
printf("the value is %x\n",inetaddr);
注意:
The inet_addr() function converts the Internet host address cp from IPv4 numbers-and-dots nota‐
tion into binary data in network byte order. If the input is invalid, INADDR_NONE (usually -1)
is returned. Use of this function is problematic because -1 is a valid address
(255.255.255.255). Avoid its use in favor of inet_aton(), inet_pton(3), or getaddrinfo(3),
which provide a cleaner way to indicate error return.
这里讲到了inet_addr的一些弊端,最后还建议用inet_aton(), inet_pton(3), or getaddrinfo(3),这些来代替使用。
inet_pton
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
inet_pton的话相对于上面的略微复杂一点,毕竟传参也多一点,但是好处是什么呢?
好处在于它是兼容IPv4和IPv6的。
int af
af 选AF_INET支持IPv4,选AF_INET6就支持IPv6
const char *src
传进去的是你要转换的地址,明显是一个输入型函数
void*dst
为什么是void*类型呢?因为它兼容两种模式,所以就得这样设置。
如果是ipv4的话,到时的dst就得跟struct in_addr挂钩,如果是ipv6的话,就要跟struct in6_addr挂钩了。这些黑体的结构体具体可以在
/usr/include/netinet/in.h里面找。
The address is converted to a struct in_addr and copied to dst, which
must be sizeof(struct in_addr) (4) bytes (32 bits) long.
这里是AF_INET的一小节description,大概就是说转换后的值就会丢到dst所指向的struct in_addr里面(这里是ipv4)
//这里开始测试inet_pton
int ret_pton = 0;
struct in_addr dst;
ret_pton = inet_pton(AF_INET,IP_ADDR,&dst);
if (ret_pton <= 0)
{
if(0 == ret_pton)
{
printf("the ipaddr is not a valid network address\n");
exit (-1);
}
else
perror("in_pton");
return -1;
}
printf("dst = %x\n",dst.s_addr);
inet_ntop
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
作用就是跟inet_pton反着来。
1.const void* src换成了pton里面的dst了,注意不要搞混,
2.char *dst可以自己定义一个buffer,然后把转换后的值放里面。
3.socklen_t size,用来规定这个buffer的长度,注意是有规定最短长度的。
这个长度我还专门测试了一下
char test1[INET6_ADDRSTRLEN];
char test2[INET_ADDRSTRLEN];
printf("sizeof test1 is %ld\nsizeof test2 is %ld\n",sizeof(test1),sizeof(test2));
test1是ipv6的,test2是ipv4的,ipv6不熟悉,用ipv4来猜测检查一下
192.168.123.123
一共3*4+3 = 15 可能最后有个\0多算一个字节一共16个字节(因为sizeof单位是字节。)
下面是inet_ntop的测试代码
//这里开始测试inet_ntop
char ntop_buffer[INET_ADDRSTRLEN];
const char * ret_p = NULL;
ret_p = inet_ntop(AF_INET,&dst,ntop_buffer,sizeof(ntop_buffer));
if (NULL == ret_p)
{
perror("inet_ntop\n");
exit(-1);
}
printf("the value is %s\n",ntop_buffer);
至此一些相关的辅助库函数都讲完了。
在学习的过程中我还发现了原来man手册inet_pton里面有一个示例代码的:
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int
main(int argc, char *argv[])
{
unsigned char buf[sizeof(struct in6_addr)];
int domain, s;
char str[INET6_ADDRSTRLEN];
if (argc != 3) {
fprintf(stderr, "Usage: %s {i4|i6|<num>} string\n", argv[0]);
exit(EXIT_FAILURE);
}
domain = (strcmp(argv[1], "i4") == 0) ? AF_INET :
(strcmp(argv[1], "i6") == 0) ? AF_INET6 : atoi(argv[1]);
s = inet_pton(domain, argv[2], buf);
if (s <= 0) {
if (s == 0)
fprintf(stderr, "Not in presentation format");
else
perror("inet_pton");
exit(EXIT_FAILURE);
}
if (inet_ntop(domain, buf, str, INET6_ADDRSTRLEN) == NULL) {
perror("inet_ntop");
exit(EXIT_FAILURE);
}
printf("%s\n", str);
exit(EXIT_SUCCESS);
}
真的写的非常的漂亮工整,很值得学习,那个socklent size的问题就是看完这个代码才从man手册里面仔细看。
printf和fprint的区别可以c_phoenix这位博主里面学习一下