之前学了一些网络编程的东西,但还没有系统看过相关书籍,故先选了这本书来读,并记下一些阅读过程中觉得值得记录的东西,作为回顾之用。这里的记录只有Linux下的相关知识,没有Windows的相关操作。
接电话套接字:套接字编程就像电话机。首先要安装电话机(socket函数),接着要给电话机分配号码(bind函数),还要给电话机接上电话线(listen),如果电话响了就可以接听电话了(accept函数)
int socket(int domain, int type, int protocol)
1. domain:协议簇,常用的就是PF_INET(IPV4互联网协议簇)
2. type(数据传输方式):
1. SOCK_STREAM :面向连接的套接字
2. SOCK_DGRAM:面向消息的套接字
3. protocol(协议的最终选择):如果前面两个参数已经确定好了协议,则最后一个参数传递0即可。
TCP套接字和UDP套接字不会共用端口号,所以允许重复。
uint16_t, in_addr等都是POSIX(可移植操作系统接口)定义的数据类型,好处是可扩展,不管到了哪里uint16_t都是两个字节的无符号数。
sin_port和sin_addr都是以网络字节序保存
sin_zero的目的是为了让sockaddr_in和结构体sockaddr保持一致而插入的成员,必须填充为0。
对于网络中数据的传输,在传输前会自动将数据转化为网络字节序,接收的数据也会自动转化为主机字节序,不需要程序员手动转化。
in_addr_t inet_addr(const char * string):将字符串信息转化为网络字节序的整数型,如果返回值等于INADDR_NONE,则出错(1.2.3.256则会出错)
int inet_iton(const char * string, struct in_addr * addr),成功返回1,失败返回0,string转化之后的值存放在addr中。
char * inet_ntoa(struct in_addr adr):将网络字节序转化为字符串形式。
这里的memset是为了让sin_zero初始化为0。
在监听服务器上所有IP地址时,可以使用INADDR_ANY。
#include
#include
#include
#include
int main(int argc, char * argv[]) {
char * ip = "127.12.11.11";
char * port = "4000";
struct sockaddr_in addr;
//这是必须的,因为最后的8位为0
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
//字符串转换为地址的第一种方法
inet_aton(ip, &addr.sin_addr);
//第二种方法:addr.sin_addr.s_addr = inet_addr(ip);
//字符串转化为整数再转化为网络字节序
addr.sin_port = htons(atoi(port));
//地址转化为字符串
char * str_ptr = inet_ntoa(addr.sin_addr);
//这里要格外注意,inet_ntoa返回一个char *,而这个char *的空间是在inet_ntoa里面静态分配的,所以inet_ntoa后面的调用会覆盖上一次的调用。所以最好先复制出来
char str_arr[20];
strcpy(str_arr, str_ptr);
printf("IP:%s\n", str_arr);
return 0;
}
int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen):成功返回0,失败返回-1。
int listen(int sockfd, int backlog):成功返回0,失败返回-1,调用listen之后进入等待连接请求状态(指客户端请求连接时,受理连接前一直使请求处于等待状态) backlog表示已完成队列的最大长度。
int accept(int sock, struct sockaddr * addr, socklen_t * addrlen):成功时返回创建的套接字描述符,失败返回-1。地址是客户端地址。
int connect(int sock, struct sockaddr * addr, socklen_t addrlen):成功返回0,失败返回-1。
服务端只有bind()没有Listen(),客户端会Connect()成功吗?
1. Connect()失败
2. bind()操作只是服务端绑定IP:Port(其他进程便无法bind()此IP:Port),并没有监听,lsof -i找不到端口对应的Fd服务端只有Listen()没有accept(),客户端会Connect()成功吗?
1. Connect()成功,且tcpdump有完整的3次握手报文
2. Listen()操作后,内核会维护一个监听队列,用于与客户端建立连接(完成3次握手),故客户端能Connect()成功。
服务端accept()会产生网络通讯吗?
1. 没有,tcpdump显示没有报文产生
2. accept()操作只是从Listen()的监听队列中取出一个连接,并建立一个新Socket用于与客户端通讯,故没有网络通讯产生。
服务端只有Listen()没有accept(),客户端Connect()成功后可以调用write()写数据吗?
1. 可以写数据,write()调用返回成功,且tcpdump有完整的请求[PSH]-应答[ACK]报文产生
2. 虽然没有accept(),但客户端依然能write()数据,这时数据存储在服务端的TCP缓冲区中,等到进程accept()之后还可以read()到
服务端只有Listen()没有accept(),客户端连接能正常断开吗?
1. 不能,客户端调用close()之后,tcpdump显示没有完整的4次握手断开报文,只有前2个报文[FIN]和[ACK]
2. 由于服务端没有accept()取出连接fd调用close(),对于服务端没有调用close()的连接,由上图可知客户端的TCP连接会停留在FIN_WAIT_2状态,一直占用客户端资源到FIN_WAIT_2状态超时
迭代服务器:反复调用accept函数
echo(回声)服务器端/客户端:将客户端传来的原封不动传回。课本上还有一个计算器的示例代码。
UDP的工作原理相当于寄信,填好寄信人和收信人的地址,放进邮筒即可。
流控制是区分UDP和TCP的最重要的标志。
UDP服务器端和客户端均只需要一个套接字,而不需要每两台电脑之间建立一个唯一的连接。
UDP存在着数据边界,先调用三次sendto, 之后服务端也得调用三次recvfrom才可以。
调用close()函数将断开输入流和输出流,不仅己方无法再发送数据,另一端发送给己方的数据也无法接收。
情况1:C(client)与S(server)建立链接之后, 当C向S发送数据之后调用shutdown来关闭写操作(断开链接的四次挥手中的前两次)告诉S, C端已发送数据完成, 此时S依然可向C发送数据.
情况2:C(client)与S(server)建议链接之后, 当C向S发送数据之后调用close来关闭socket(同样发送断开链接的四次挥手的前两次, 后两次挥手将由S端调用close来完成), 此时S端被其他条件阻赛并不调用close函数. 然后此时S端向C端发送数据将会引起C端回应rst数据包.
我对shutdown和close跟四次挥手关系的理解
为何需要半关闭
客户端要接收到服务端所有信息后返回一个数据报,为了判断服务端是否已经传输完成,需要用到半关闭(服务端关闭输出流)让客户端读不到数据了,这样客户端才知道读完了然后就返回数据报给服务端。
将域名写入程序比将IP地址写入程序好,因为IP地址会经常变更,但是域名一般不会。
h_addr_list中存的指针是指向in_addr结构体变量地址值而非字符串。
#include
#include
#include
#include
#include
int main(int argc, char * argv[]) {
char * ip = "www.zhougb3.cn";
struct hostent * host = gethostbyname(ip);
printf("%s\n", inet_ntoa(*(struct in_addr *)(host->h_addr_list[0])));
return 0;
}
套接字有多种可选项,并且是按协议层分的,有SOL_SOCKET,IPPROTO_IP,IPPROTO_TCP
套接字类型只能在创建时决定,以后不能再更改。
我们可以通过这两种方法读取和更改IO缓冲大小,但是更改IO缓冲大小时,并不一定能够按照我们的要求更改,我们只是表达了我们要更改的需求。
binding error : 当服务器主动断开连接之后进入TIME_WAIT状态,这个时候该端口在短时间内不可用,使用binding函数的话就会报错。
如果断开连接的主动发起方在TIME_WAIT状态时收到重复的FIN报文,则会重启定时器,因此可能导致TIME_WAIT状态一直持续。
内核在处理一个设置了SO_REUSEADDR的socket绑定时,如果其绑定的ip和port和一个处于TIME_WAIT状态的socket冲突时,内核将忽略这种冲突,即改变了系统对处于TIME_WAIT状态的socket的看待方式。
nagle算法
每个进程都有一个进程ID,1要分配给操作系统启动后的第一个进程,用户进程无法得到ID值1。
调用fork函数后,子进程返回0,父进程返回子进程ID。
僵尸进程:会占用系统资源。
子进程终止方式: 调用exit函数,在main函数中return。
只有当父进程主动发起请求时,操作系统才会将子进程返回值给父进程,否则子进程会一直保留为僵尸进程(或者一直保留到父进程也终止了才跟着一起终止)。
调用wait函数时,如果没有已经终止的子进程,那么程序将阻塞直到有子进程终止。
父子进程都要进行繁忙的工作,那父进程就不可能一直调用wait函数来中止子进程,而我们同时也要去除僵尸进程,那应该怎么做呢?(信号处理,由操作系统告知)
该程序执行时间很短,因为每次产生信号时都会唤醒进程,进程一旦被唤醒就不会再进入休眠状态。
为什么要完成双向通信需要两个管道?
如果两个进程同时使用一个管道的读端和写端,那么可能一个进程写的东西会被自己读走。
这个很重要,但已经掌握的比较熟练了,就不多叙述。
常见的可选项(flag)
MSG_OOB:TCP不存在真正意义上的带外数据,只利用TCP的紧急模式(督促数据接收对象尽快处理数据,仍然保持传输顺序)进行传输。
带外数据的应用情况
如果发送客户端程序由于一些原因需要取消已经写入服务器的请求,那么他就需要向服务器紧急发送一个标识取消的请求。使用带外数据的实际程序例子就是telnet,rlogin,ftp命令。前两个程序(telnet和rlogin)会将中止字符作为紧急数据发送到远程端。这会允许远程端冲洗所有未处理的输入,并且丢弃所有未发送的终端输出。这会快速中断一个向我们屏幕发送大量数据 的运行进程。ftp命令使用带外数据来中断一个文件的传输。
服务器端程序里面捕捉SIGURG信号来及时接受带外数据,带外数据只能有一个byte。
MSG_PEEK和MSG_DONTWAIT验证输入缓冲中是否存在接收的数据。MSG_PEEK读取了缓冲中数据也不会删除,MSG_DONTWAIT以非阻塞方式读。
使用readv和writev可以将位于不同缓冲区中的数据(数组)一起发送,减少调用IO次数,提高效率。
使用C语言的标准IO:移植性,利用缓冲提高性能(使用IO函数缓冲,可以减少向套接字缓冲移动 次数,自然性能有所提升)。
流和FILE对象
标准IO库:是围绕流进行的,当打开一个流时,标准IO函数fopen返回一个指向FILE对象的指针,而FILE是一个结构体,包含着管理该流需要的所有信息,其中就包含文件描述符,指向缓冲区的指针,缓冲区的长度,出错标志等。
系统调用:是围绕文件描述符,当打开一个文件,即返回一个文件描述符,然后该文件描述符就用于后续的操作。
在将文件描述符变为文件指针时,我们根据读和写分别返回了两个文件指针,关闭任何一个都会完全终止套接字,因此我们要学会对FILE指针进行半关闭操作。
创建file指针前先复制文件描述符即可,只有文件描述符的数量为0,套接字才会销毁。
当主线程(进程)执行完毕后,子线程如果还没执行完毕也会被中止。下图即是等待线程中止。
工作线程模型
互斥量和信号量:互斥量是在同一个线程里面上锁解锁,而信号量(二进制)一般在两个线程间,一边上锁一边解锁。
线程不像进程,一个进程中的线程之间是没有父子之分的,都是平级关系。即线程都是一样的, 退出了一个不会影响另外一个。但是所谓的”主线程”main,其入口代码是类似这样的方式调用main的:exit(main(…))。main执行完之后, 会调用exit()。exit() 会让整个进程over终止,那所有线程自然都会退出。
如果进程中的任一线程调用了exit,_Exit或者_exit,那么整个进程就会终止。
参考:https://blog.csdn.net/inuyashaw/article/details/53465294
综合以上要想让子线程总能完整执行(不会中途退出),一种方法是在主线程中调用pthread_join对其等待,即pthread_create/pthread_join/pthread_exit或return;一种方法是在主线程退出时使用pthread_exit,这样子线程能继续执行,即pthread_create/pthread_detach/pthread_exit;还有一种是pthread_create/pthread_detach/return,这时就要保证主线程不能退出,至少是子线程完成前不能退出。现在的项目中用的就是第三种方法,主线程是一个死循环,子线程有的是死循环有的不是。
课本中还有一个简单的聊天软件
最后面还有一个http服务器