Preview
基于上一篇博客,本文将继续展开TCP面向连接的,客户端以及服务端各自需要进行的操作,我们按照真实TCP连接的顺序,分别阐述客户端socket(), connect()以及服务端socket(), bind(), listen(), accept()建立连接的过程。连接建立之后,阐述send(), recv()的具体细节。
Create Socket
UNIX系统万物皆文件的思想,引入了重要的文件描述符概念,详情可以阅读CS:APP的UNIX I/O章节。简单类比,可以将文件描述符看作一个指针数组的index,指针数组指向的内容与文件相关。
在socket编程中,有两种方式创建新的套接字并获取对应的文件描述符,socket()以及accept(),本章节主要介绍socket()
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
可以理解,创立一个套接字,必须要获得协议相关内容,例如指明TCP/IP协议。
本博客主要针对TCP,所以以此陈述。
相应的,domain就代表连接使用的是IPv4还是IPv6。那么type就对应的是SOCK_STREAM。protocol就需要是知名是tcp还是UDP(其实type等同于TCP/UDP不太精准,只是说TCP是基于SOCK_STREAM),这个可以利用getprotobyname()函数获取。
事实上,socket的三个参数我们是利用getaddrinfo()获取的关于addrinfo链表写入的(真就工具人呗)
- domain: ai_family
- type: ai_socktype
- protocol: ai_protocol
关于domain,这里又有一段历史...
domain
isPF_INET
orPF_INET6
This
PF_INET
thing is a close relative of theAF_INET
that you can use when initializing thesin_family
field in yourstruct sockaddr_in
. In fact, they’re so closely related that they actually have the same value, and many programmers will callsocket()
and passAF_INET
as the first argument instead ofPF_INET
. Now, get some milk and cookies, because it’s time for a story. Once upon a time, a long time ago, it was thought that maybe an address family (what the “AF” in “AF_INET
” stands for) might support several protocols that were referred to by their protocol family (what the “PF” in “PF_INET
” stands for). That didn’t happen. And they all lived happily ever after, The End. So the most correct thing to do is to useAF_INET
in yourstruct sockaddr_in
andPF_INET
in your call tosocket()
.
Client
先说简单而无脑的客户端,TCP的3次握手总得有人先握手,connect()便是开启握手过程的函数
connect()
开始和人打招呼,得先知道别人在哪,对应互联网就是套接字地址,利用上一篇博客的内容就可以轻松愉快的获得了。
深入这些之后就发现,逐步和计网的课正在结合起来,getaddrinfo()有些类似于DNS域名解析,connect()就类似于开始握手。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
函数的参数是好理解的,你需告知系统,是哪个套接字,去找谁,开启连接,此处需要addrlen(),即sizeof( *serv_addr)
,应该是函数内部具有更多细节。
这里就可以给出结合socket(), connect()客户端发起连接的一系列准备工作了
struct addrinfo hints, *res;
int sockfd;
memset(&hints, 0, sizeof(hints));
hints.ai_family= AF_UNSPEC;
hints.ai_socketype= SOCK_STREAM;
getaddrinfo("www.example.com", "3490", &hints, &res);
sockfd= socket(res->ai_family, res->ai_socktype, res->ai_protocol);
connect(sockfd, res->ai_addr, res->ai_addrlen);
Server
服务器的活就多了,因为需要考虑让很多人来连接,所以需要固定端口号(bind()), 默认套接字打开是用来找别人的(CS:APP话来说,主动套接字),需要改编为可以监听别人进来的数据(listen()),接受以后,计网知识对应的,需打开连接套接字(accept())。
bind()
在前面客户端部分未陈述,事实上,内核对于套接字的端口开始是随便分的,排除已经使用的,以及周知端口随机分配。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
很简单的参数设置,和哪个套接字bind(), 把这个套接字bind()上的地址,还有也许出于函数设置的addrlen: sizeof(*my_addr)
先看看老派的做法:
int sockfd;
struct sockaddr_in my_addr;
sockfd= socket(PF_INET, SOCK_STREAM, 0);
my_addr.sin_family= AF_INET;
inet_pton(AF_INET, "10.12.110.57", &(my_addr.sin_addr));
// actually older way is my_addr.sin_addr.s_addr=inet_addr("10.12.110.57");
// or my_addr.sin_addr.s_addr= INADDR_ANY;
my_addr.sin_port= htons(MYPORT);
memset(my_addr.sin_zero, 0, sizeof(my_addr))
还是换工具人上场吧
struct addrinfo hints, *res;
int sockfd;
memset(&hints, 0, sizeof(hints));
hints.ai_family= AF_UNSPEC;
hints.ai_socktype= SOCK_STREAM;
hints.ai_flags= AI_PASSIVE;
getaddrinfo(NULL, "3490", &hints, &res);
sockfd= socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
这就将上一篇博客提到的模板连接起来了。
listen()
话不多,上定义
int listen(int sockfd, int backlog)
指定好两件事,让谁监听,最多能处理几个,这就分别对应了sockfd, backlog。
accept()
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
这里就是涉及到计网的知识了,TCP面向连接时服务器端,是专门利用一个套接字监听,称为监听套接字,再利用fork()(CS:APP异常章节),创建了新的连接套接字来和客户端交互,这样做也好理解,例如一个web应用,总不可能全世界每时每刻就让一个人连接他。
顾名思义,猜测这个函数应该还有发送回去ACK的功能
accept()参数是这样设置的,从哪个监听套接字收到了连接请求?我总得知道这个连接是哪来的?以及老生常谈的addrlen
Easy enough.
addr
will usually be a pointer to a localstruct sockaddr_storage
. This is where the information about the incoming connection will go (and with it you can determine which host is calling you from which port).addrlen
is a local integer variable that should be set tosizeof(struct sockaddr_storage)
before its address is passed toaccept()
.accept()
will not put more than that many bytes intoaddr
. If it puts fewer in, it’ll change the value ofaddrlen
to reflect that.
这里就有细节需要注意,他是记录到sockaddr_storage结构里,前面介绍过,这样IPv4, IPv6通吃,addrlen设置也很有意思,相当于是一个放入地址上限的意思,但是放少了,又会把他改掉。
Communication
前面连接没问题,就开始各种交流吧
这两个函数针对的是stream socket,就是设置了SOCK_STREAM的。
send()
int send(int sockfd, const void *msg, int len, int flags);
你需要通过哪个套接字帮你发送消息(你把待发信息交给他处理)(sockfd)?处理的信息是啥(msg)?发多少(len)?发送姿势是啥(通常为0,遇事不决man一下)?
recv()
int recv(int sockfd, void *buf, int len, int flags);
你想从哪个套接字接受发过来的数据(sockfd)?放到哪(buf)?最多能接受多少(len,注意这里和send()是不同的,这里是最多 可以接受多少信息)?接受姿势是啥(通常也是0)?
Conclusion
至此,你已经可以写一个简单的类似于OICQ之类的玩意了,关于TCP的socket()编程简单介绍就结束了,随后会加上示例代码。