Linux网络编程(2)

服务器

浏览数:41

2020-6-11

AD:资源代下载服务

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 is PF_INET or PF_INET6

This PF_INET thing is a close relative of the AF_INET that you can use when initializing the sin_family field in your struct sockaddr_in. In fact, they’re so closely related that they actually have the same value, and many programmers will call socket() and pass AF_INET as the first argument instead of PF_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 use AF_INET in your struct sockaddr_in and PF_INET in your call to socket().

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 local struct 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 to sizeof(struct sockaddr_storage) before its address is passed to accept(). accept() will not put more than that many bytes into addr. If it puts fewer in, it’ll change the value of addrlen 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()编程简单介绍就结束了,随后会加上示例代码。

作者:IdiotNe