《UNIX 网络编程 卷1:套接字联网API 》笔记

第一部分 TCP/IP

TCP是一个复杂、可靠的字节流协议,而UDP是一个简单、不可靠的数据包协议。

理解connect、accept、close函数
这里写图片描述

这里写图片描述

这里写图片描述

TIME_WAIT状态

最长分节生命期:2MSL。

TIME_WAIT状态存在的两个理由:
1.可靠地实现TCP全双工连接的终止。
2.允许老的重复分节在网络中消逝。

端口号:

16位。范围0~65535。
0~1023 :众所周知端口。
1024~49151 :已登记的端口。
49152~65535:动态的或私用的端口。

常见端口号:
HTTP 80
Telnet 23
FTP  20、21
SMTP 25
POP3 110
TFTP 69

ICMPx协议实现的网络诊断应用: ping 和 traceroute。

第二部分

第3章 套接字简介

1.IPV4套接字地址结构

        struct  in_addr{
            in_addr_t s_addr; //32-bit
        };

        struct   sockaddr_in   {  
                uint8_t sin_len;
                sa_family_t sin_family;
                in_port_t sin_port; //16-bit
                struct in_addr sin_addr; //32-bit
                char sin_zero[8];
        };  
        struct   in_addr就是32位IP地址。  

IPV4地址和TCP/UDP端口号在套接字地址结构中总是以网络字节序来存储(大端模式)。

这里写图片描述

2.主机字节序和网络字节序之间转换函数

主机转网络字节序:
htons(); //16位               htonl(); //32位
网络转主机字节序
ntohs(); //16位               ntohl(); //32

3.字节操纵函数

void bzero(void *dest ,size_t nbytes); //清零
void *memcpy(void *dest,const void *src,size_t nbytes);//内存拷贝
当源字节串与目标字节串重叠时 用memmove函数而不用那个memcpy函数。

inet_pton和inet_ntop函数
进行点分十进制数串和ip网络字节序二进制值间转换地址。IPV4和IPV6地址都适用。
#include<arpa/inet.h>

int inet_pton(int family, const char*strptr, void *addrptr);
//成功返回1,出错为-1

const char*inet_ntop(int family, const void *addrptr, char *strptr, size_len);
//成功返回指向结果的指针,出粗偶返回NULL

第4章 基本TCP套接字编程

套接字编程之服务器模型
这里写图片描述

套接字之客户端编程模型
这里写图片描述

1.socket函数

int socket(int family, int type, int protocol);

//成功返回监听描述符。用来设置监听,出错为-1
family是表示socket使用的协议类型,ipv4的话用AF_INET, 6的话AF_INET6type是创建的套接字类型,至少三种SOCK_STREAM(字节流套接字), SOCK_DGRAM(s数据包套接字), SOCK_RAW(原始套接字)。
protocol协议的标识,一般为0让系统选择,如果是原始套接字就要在这里进行设置以区分是链路层还是网络层或者传输层。

2.bind函数

用来将前面的监听描述符和服务器ip和端口绑定,设置服务器ip和端口。

int bind(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);
//成功返回0 ,出错为-1

addr是套接字地址结构体的指针(地址),需要从sockaddr_in强制转换过来。
bind()函数可以指定IP地址或端口号,也可以两者都指定,也可以都不指定。

3.listen函数

int listen(int sockfd, int backlog);
//成功返回0 ,出错为-1

listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。
backlog是队列中已经完成三次握手和未完成三次握手的所有连接个数。

4.accept函数

前面的步骤只是完成三次握手,然后将一条条连接放入队列,这一步从队列中拿出一个进行通信。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
//成功返回非负描述符,错误返回-1

//accept(listenfd,NULL,NULL);//不关心客户的地址

cliaddr用来返回客户端的地址结构体。
addrlen是一个指针,暗示着用来返回某个值,就是客户端地址结构体的大小。
输入的时候需要给一个初值(用来装地址结构体的对象的大小),称为值--结果型参数。
成功返回值是一个可以用来read/write的已连接套接字描述符。

这里写图片描述
5.connect函数

int connect(int sockfd, const struct sockaddr *seraddr, socklen_t addrlen);
//成功返回0 ,出错为-1

seraddr是包含了服务器的ip和端口的地址结构体。
连接后sockfd就是用来writeread的fd。

6.fork 和 exec函数

#include <unistd.h>

pid_t fork(void);

//返回值:在子进程中为0,父进程中为子进程ID,出错则为-1

fork()函数调用一次,返回两次。

fork两个典型用法:

1.一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。
2.一个进程执行另外一个进程。调用fork产生子进程,再调用exec把自身替换成新程序。

7.getsockname 和 getpeername 函数

#include <sys/socket.h>

int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen);
//成功为0 ,出错为-1
//getsockname 用于获取某个套接字的地址族。用于返回由内核赋予该连接的本地IP地址。

int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addelen);
//成功为0 ,出错为-1

//当一个服务器是由调用过accept的某个进程通过exec执行程序时,它能够获取客户身份的唯一途径就是调用getpeername

第5章 TCP客户端/服务器程序示例

1.POXI信号处理

信号的发送/产生
1)硬件(内存读取)错误时产生,如段错误信号。
2)在有终端的时候,键盘组合产生,如SIGINT和SIGQUIT
3)软件错误时产生,如除0错误信号
4)使用kill系统调用发送
5)使用raise给自己发送信号。
6)闹钟到期后发生,给自己发送

收到信号后的响应
1)默认行为(终止本进程)
2)当信号不存在,不理睬。例如,父进程在收到子进程发来的SIGCHLD信号。
3)忽略信号。经过signal函数的设置,将信号完全消灭掉。
4)安装处理函数,处理这个信号。

两个特殊的信号是无法忽略和处理的,SIGKILL(9) 和SIGSTOP(19)
waitwaitpid函数
pid_t wait(int *staloc);
pid_t waitpid(pid_t pid,int *ststloc ,int options);
/*返回:成功返回进程ID,出错为0或-1*/

pid:从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。     
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。

pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpidwait的作用一模一样。   

pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。

pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。   

options: options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用

fork子进程时,必须捕获SIGCHLD信号

区别:当多个信号同时要处理时,用waitpid而不是wait

2.服务器的几种异常终止

1、在accept函数返回前连接夭折

这种情况发生在TCP 3次握手刚好完成,服务器TCP将连接放入到已经建立好连接队列中,此时客户端给一个RST,接下来accept返回,不过这时accept返回的是ECONNECTABORT错误.这不是一个致命错误。

2、服务器进程终止

过程如下:

a、kill掉服务子进程的进程ID,作为进程善后处理的部分,所有打开的文件描述符被关闭,这导致服务端TCP发送FIN给客户端,客户端TCP响应以ACK。

b、客户端此时正阻塞在fgets函数调用上,这导致客户端不知道服务端TCP已经关闭连接。

c、客户端在fgets返回后调用write向服务端发数据,由于服务端已经被kill掉,所以服务端TCP会发送一个RST给客户端TCP.

d、客户端在发送完数据后立即调用read读取数据,由于有第一步的FIN,read立即返回0(表示EOF),然而客户端希望的是收到刚才发送的数据而不是EOF。如果客户端接着往服务端发数据,将诱发服务端TCP向服务端发送SIGPIPE信号,因为向接收到RST的套接口写数据都会收到此信号.

问题的本质在于客户端同时处理两个描述字–套接口和用户输入,程序被单纯地阻塞在一个源上了。这个问题可以通过1、设置非阻塞模式。2、采用select以及epoll处理。

3、服务器主机崩溃

在客户TCP发送数据后,由于接收不到ACK,它将试图一直重传,直到最后放弃,并返回给客户进程一个出错信息。ETIMEOUT表示没有相应,EHOSTUNREACH表示路由器判定主机不可达。

4、服务器崩溃后重启

由于服务端TCP丢失了以前的连接信息,这将导致服务端发送一个RST,而此时客户端阻塞在read函数,这将导致返回一个ECONNECTRESET错误.

5、服务器关机

服务器关机时init进程会先发送SIGTERM(此信号可捕获)给所有进程,再过一段时间发送SIGKILL(次信号不可捕获)给仍然在运行的程序,这时就和服务器进程终止一样了。必须在客户中使用select或poll函数,使得服务器的终止一经发生,客户就能检测到。

第6章 I/O复用:select 和 epoll 函数

1.Unix下5种I/O模型

  • 阻塞型I/O
    默认情况下,所有套接字都是阻塞的。
  • 非阻塞型I/O
    使用read的时候,没有数据可读,则立即返回, 错误码是EWOULDBLOCK。这种情况需要多次去read

  • I/O复用(select 和 epoll)
    通过某个观察者来观察究竟哪个fd已经准备好,将相应的fd通知给应用程序。这种机制可以等待多个fd准备好。

  • 信号驱动I/O(SIGIO)
    一旦资源可用就发出一个信号,然后用信号处理函数来处理它。
    这个信号就是SIGIO信号。

  • 异步I/O

2.I/O复用(select 和 epoll)

1、select机制

使用select之前要将fd放入集合然后传入select,select返回后需要逐个判断,看看是哪个描述符可用。
select返回后,如果还要再次select则需要重新将fd再传进去


int select(int nfds, 
fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
struct timeval *timeout);

注意:nfds参数为待测试的最大描述符加1,timeout参数为NULL永远等待,通过结构体struct timeval可以设置等待的时间的秒数和微秒数。

void FD_CLR(int fd, fd_set *set);  //将某个描述符清除
int  FD_ISSET(int fd, fd_set *set); //判断是否已经置位,也就是是否保存
void FD_SET(int fd, fd_set *set);  //将描述符放入集合
void FD_ZERO(fd_set *set);      //清除所有的位,描述符的初始化很重要。

SELECT 客户端

#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>



int main(void)
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);

    struct sockaddr_in sin_server;
    bzero(&sin_server,sizeof(sin_server));
    sin_server.sin_family=AF_INET;
    sin_server.sin_port=htons(2018);
    inet_pton(AF_INET,"127.0.0.1", &sin_server.sin_addr);

    int ret=connect(sockfd,(struct sockaddr *)&sin_server,sizeof(sin_server));
    if(ret==-1)
    {
        perror("connect");
    }
   while(1)
   {
        fd_set fdread;
        FD_ZERO(&fdread);
        FD_SET(0,&fdread);
        FD_SET(sockfd,&fdread);
        select(sockfd+1,
                &fdread,NULL,NULL,
                NULL);
        //判断是否0号描述符准备好
        if(FD_ISSET(0,&fdread))
        {
            char buffer[1024]={0};
            read(0,buffer,sizeof(buffer));
            write(sockfd,buffer,strlen(buffer));       
            printf("字符串已发送\n"); 
        }
        //判断是否sockfd准备好
        if(FD_ISSET(sockfd,&fdread))
        {
            char buffer[1024]={0};
            ret=read(sockfd,buffer,sizeof(buffer));
            printf("读到的数据是%s\n",buffer);
            close(sockfd);
        }
   }
}

2.epoll函数

常用函数: 
int epoll_create(int size);   //创建一个epoll对象,返回的是它的描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  //用来将文件描述符加入epoll对象
epfd是前面创建的epoll对象
op有如下选项:
EPOLL_CTL_ADD, EPOLL_CTL_DEL, EPOLL_CT_MOD

struct epoll_event的结构体如下:
  struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };
typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;


events有如下选项:
EPOLLIN: 表示该描述符是用来读的,
EPOLLOUT: 相反
EPOLLLT: 水平触发, 有数据后会一直提醒
EPOLLET: 边缘触发, 有数据的时候只会提醒一次,就好比上升沿只有一次一样

data是一个联合体,通常只使用里面的fd这个值




int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//等待,相当于select

events用来返回可用epoll_event的结构体, 
maxevents是结构体的数组的大小。
timeout是超时时间,单位是ms,如果为-1,表示永远等待,0不等待。

第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。

第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

epoll客户端

#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>


int main(void)
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);

    struct sockaddr_in sin_server;
    bzero(&sin_server,sizeof(sin_server));
    sin_server.sin_family=AF_INET;
    sin_server.sin_port=htons(2018);
    inet_pton(AF_INET,"127.0.0.1", &sin_server.sin_addr);

    int ret=connect(sockfd,(struct sockaddr *)&sin_server,sizeof(sin_server));
    if(ret==-1)
    {
        perror("connect");
    }

    struct epoll_event ev, resutl[10];
    int epfd=epoll_create(256);
    ev.data.fd=0;
    ev.events=EPOLLIN;//用来监测输入
    epoll_ctl(epfd,EPOLL_CTL_ADD,0,&ev);

    ev.data.fd=sockfd;
    ev.events=EPOLLIN;
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);    

   while(1)
   {
       //n表示有多少个事件准备好了,如果0则是超时返回
       int n=epoll_wait(epfd,resutl,10,-1);
       int i;
        for(i=0;i<n;i++)
        {
            //判断是否0号描述符准备好
            if(resutl[i].data.fd==0)
            {
                char buffer[1024]={0};
                read(0,buffer,sizeof(buffer));
                write(sockfd,buffer,strlen(buffer));       
                printf("字符串已发送\n"); 
            }
            //判断是否sockfd准备好
            if(resutl[i].data.fd==sockfd)
            {
                char buffer[1024]={0};
                ret=read(sockfd,buffer,sizeof(buffer));
                printf("读到的数据是%s\n",buffer);
                close(sockfd);
            }
        }
   }
}





3.shutdown函数

shutdown函数可以不管引用计数就激发TCP的正常连接终止序列。

int shutdown(int sockfd,int howto);
//成功为0 ,出错为-1

howto参数
SHUT_RD 关闭连接的读这一半
SHUT_WR 关闭连接的写这一半
SHUT_RDER 连接的读半部和写半部都关闭

第7章 套接字选项

1.getsockopt 和 setsockopt 函数

#include <sys/socket.h>

int getsockopt(int sockfd,int level,int optname,void *optval
                socklen_t *optlen);/*获取*/

int setsockopt(int sockfd,int level,int optname,const void *optval,
                socklen_t optlen);/*设置*/
/*均返回:成功为0 ,出错为-1*/

sockfd指向打开的描述符,level指定系统中解释选项的代码或通用套接字代码,
optval是一个指向某个变量(*optval)的指针,*optval 0表示禁用相应选项,非0
表示相应选项被启用。

level:SOL_SOCKET 、IPPROTO_IP等。
optname:SOL_SOCKET级别中有 SO_LINGER、SO_REUSEADDR、SO_RCVBUF等。

2.SO_LINGER选项

本选项指定close函数对面向连接的协议如何操作。默认close立即返回,如果有数据残留在套接字发送缓冲区中,系统将试着把这些数据发送给对端。

struct linger{
    int l_onoff;
    int l_linger;
};

开启选项的情况下:
    l_onoff   l_linger
    false      X            l_linger值无关
    true       0            不延迟, 产生rst信号        
    true       >0           延迟多少秒再关闭

这里写图片描述
close立即返回,不等待

这里写图片描述
close拖延到了对于客户端FIN的ACK才返回
这里写图片描述

3.SO_RCVBUF 和 SO_SNDBUF选项

改变发送缓冲区和接收缓冲区的大小。

对于客户端:SO_RCVBUF选项必须在调用connect之前设置。
对于服务器:必须在调用listen之前给出监听套接字设置。

4.SO_REUSEADDR 和 SO_REUSEPORT 选项

地址复用:在tcp关闭的时候,会经历一个time_wait状态,在此期间,同一个ip地址+端口是不能再次使用的。

SO_REUSEADDR 允许单个进程捆绑同一端口到多个套接字上(只要每次捆绑指定不同的本地ip地址即可),允许完全重复的捆绑。

5.TCP_NODELAY 选项

本选项禁用TCP的Nagle算法。

不合适使用Nagle算法和TCP的ACK延滞算法的客户是以若干小片数据项服务器发送单个逻辑请求的客户。

可以使用writev,单个writev调用最终导致调用TCP输出功能一次而不是两次。

第8章 基本UDP套接字编程

1.recvfrom 和 sendto 函数

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                   struct sockaddr *src_addr, socklen_t *addrlen);
/*不关心数据发送者地址,后两个参数置NULL*/
/*数据包丢失客户端会永远阻塞与recvfrom调用的解决办法,给客户的recvfrom调用设置一个超时*/

ssize_t sendto(int sockfd, void *buf, size_t len, int flags,
                   struct sockaddr *dst_addr, socklen_t *addrlen);
/*均返回:成功为读或写的字节数,出错为-1*/

2.UDP 服务端和客户端模型
这里写图片描述

这里写图片描述

3.UDP 的connect函数

对于已连接UDP套接字调用connect的结果

1.不能再给输出操作指定目的的IP地址和端口号。也就是不使用sendto,而用writesend2.不必使用recvfrom以获取数据包的发送,改用readrecv或recvmsg。
3.由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。

第11章 名字与地址转换

1.DNS

域名系统(Domain Name System,DNS):用于主机名字与IP地址之间的转换。

主机名:可以是简单名字或者全限定域名。

 客户端和服务器等应用程序通过调用解析器(函数库中的函数)接触DNS服务器。

2.gethostbyname 和 gethostbyaddr 函数

#incude <netdb.h>

struct hosten *gethostbyname(const char *hostname);

返回:成功返回非空指针(只能返回IPV4地址),出错为NULL且设置h_errno,
用hstrerror函数解释这个h_errno

返回的非空指针指向如下的hostent结构
struct hostent{
    char *h_name; //主机规范名字
    char **h_aliases; //别名
    int h_addrtype; // AF_INET :host address type
    int h_length; //地址长度
    char **h_addr_list; //地址列表
};

struct hosten *gethostbyaddr(const char *addr,socklen_t len,int family);

addr 不是char*类型,而是一个指向存放IPV4地址的某个in_addr结构的指针。

2.getservbyname 和 getservbyport 函数

处理服务名和端口号的常用函数是getservvyname,接受一个服务器名作为参数,并返回包含相应端口号的结构。

#inclulde <netdb.h>

struct servent *getsservbyname(const char *servname,const char *protoname);

例:
struct servent *sptr;
sptr = getservbyname("ftp","tcp");


返回:成功为非空指针,出错为NULL

servent 结构体如下

struct servent{
    char *s_name; //规范名字
    char **s_aliases; //别名列表
    int s_port; //端口号 ,网络字节序
    char *s_proto; //使用的协议
};


struct servent *getservbyport(int port,const char *protoname);
例:

struct servent *sptr;
sptr= getservbyport(htons(53),"tcp");

第13章 守护进程和inetd超级服务器

守护进程(daemon):在后台进行并独立于所有终端控制的进程。

1.syslog 函数

#include <syslog.h>

void syslog(int priority,const char *message,...);
//具体参数的意义可以man看一下。
用于从守护进程中登记消息。因为守护进程没有控制终端,所以不能把消息fprintf到stderr上。

void openlog(const char*ident,int options,int facility);
// ident 程序名  ,options具体选项man看一下,facility默认0为LOG_USER
void closelog(void);

openlog可以在首次调用syslog前调用,closelog在应用进程不需要再发送日志消息时调用。

2.守护进程的创建

(1)调用fork让程序转入后台运行。

(2)脱离终端。
用setsid设置会话组长。

(3)再次fork以避免无意中获得新的控制终端

(4)改变工作目录和文件创建模式掩码
chdir("/")umask(0)

(5)关闭所有非必要的文件描述符。将0,1,2描述符转到null文件中。

    for(i=0;i<1024;i++)
    {
        close(i);
    }

    open("/dev/null",O_RDONLY);//0
    open("/dev/null",O_RDWR);//1
    open("/dev/null",O_RDWR);//2

    openlog(pname,LOG_PID,facility);

(6)屏蔽SIGHUP信号
signal(SIGHUP,SIG_IGN);


代码实现:

daemon_init(const char *pname,int facility)
{
    int i;
    pid_t pid;

    if((pid = fork()) < 0)
        return -1;
    else if(pid)
        exit(0);

    if(setsid()<0)
        return -1;

    signal(SIGHUP,SIG_IGN);

    if((pid = fork()) < 0)
        return -1;
    else if(pid)
        exit(0);

    chdir("/");

    for(i=0;i<1024;i++)
    {
        close(i);
    }

    open("/dev/null",O_RDONLY);//0
    open("/dev/null",O_RDWR);//1
    open("/dev/null",O_RDWR);//2

    openlog(pname,LOG_PID,facility);
    return 0;
}

3.inetd 守护进程

这里写图片描述

第 14 章 高级 I/O 函数

1.套接字的I/O上设置超时的3种方法

1)调用alram,在指定超时期长生SIGALRM信号。

可用SIGALRM 为 connect 、recvfrom设置超时。

(2)在select中阻塞等待I/O,以代替直接在readwrite上调用。


(3)使用新的SO_RCVTIMEO 和 SO_SNDTIMEO 套接字选项。但问题在于并非所有实现都支持这两个套接字选项。

两者都不能用于为connect设置超时。

2.recvmsg 和 sendmsg 函数

最通用的I/O函数。

#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);
返回:读入或写出字节数——成功;-1——出错

msghdr结构:

struct msghdr {
    void            *msg_name;           /* protocol address */
    socklen_t        msg_namelen;        /* size of protocol address */
    struct iovec    *msg_iov;            /* scatter/gather array */
    int              msg_iovlen;         /* # elements in msg_iov */
    void            *msg_control;        /* ancillary data (cmsghdr struct) */
    socklen_t        msg_controllen;     /* length of ancillary data */
    int              msg_flags;          /* flags returned by recvmsg() */
};


msg_name和msg_namelen用于套接字未连接的场合,不指明协议地址,masg_name置NULL

msg_iov 和msg_iovlen指定输入或输出缓冲区数组。

msg_control 和 msg_controllen指定可选的辅助数据的位置和大小。

只有recvmsg使用msg_flags成员。(查表)

3.标准I/O函数库的三类缓冲

1)完全缓冲。

标准输入和标准输出,除非他们指代中断设备。
缓冲区满,进程显示调用fflush或进程调用exit终止自身。

(2)行缓冲。

碰到换行符,进程调用fflush或进程调用exit终止自身。


(3)不缓冲。
每次调用标准I/O输出函数都发生I/O。错误输出。

第15章 Unix域协议

Unix域套接字:
用于在同一主机上的不同进程之间传递描述符,Unix域套接字往往比通信两端位于同一个主机的TCP套接字快出一倍。

地址结构:
#include <sys/un.h>

struct sockaddr_un{
    sa_family_t sun_family; /*AF_LOCAL*/
    char sun_path[1024]; /*null-terminated pathname */
}
sun_path数组中的绝对路径名必须以空字符结尾。如果系统中已存在该路径名,调用ulink删除这个路径名。

第16章 非阻塞式I/O

使用read的时候,没有数据可读,则立即返回, 错误码是EWOULDBLOCK。这种情况需要多次去read。对于write也是一样的。

设置方法:
使用fcntl来设置,将文件的选项设置为O_NONBLOCK。

int fcntl(int fd, int cmd, ... /* arg */ );

读取文件打开标志:F_GETFL
设置文件打开标志:F_SETFL
/* 
设置非阻塞,参数是需要设置的fd
返回值如果>=0则表示成功
失败返回-1,错误保存保存在错误码中
 */
int set_nonblock(int fd)
{
    //设置非阻塞
    //拿出原有的标识位
    int flag=fcntl(fd,F_GETFL);
    flag|=O_NONBLOCK;//加上非阻塞标识位
    //再写回去
    int ret=fcntl(fd,F_SETFL,flag);
    if(ret==-1)
    {
        //设置一下错误码
        errno=EIO;
        return -1;
    }
    return fd;
}

第17章 ioctl操作

ioctl函数     


#include <unistd.h>
int ioctl(int fd,int request,...../* void *arg  /);

//rquest 参数查表
//返回:若成功则为0.失败则为-1
        其中第三个参数总是一个指针,但指针的类型依赖于request参数。我们可以把和网络相关的请求划分为6类:


        1)套接字操作

        要求ioctl的第三个参数为指向某个整数的一个指针。

        2)文件操作

        要求ioctl的第三个参数指向一个整数。

        3)接口操作

        4)ARP高速缓存操作

        5)路由表操作

        6)流系统

这里写图片描述

第24章 带外数据

1.TCP带外数据

TCP并没有真正的带外数据,而是提供了紧急模式。
这里写图片描述
然后以MSG_OOB标志调用send函数写字符a的单字节带外数据

send(fd,"a",1,MSG_OBB);

这里写图片描述
TCP首部设置URG标志,并把紧急偏移字段设置为指向带外字节之后的字节。

OBB是否发送取决于在套接字发送缓冲区中先于它的字节数、TCP准备发送给对端的分节大小以及对端通告的当前窗口。

如果发送多个字节的带外数据

send(fd,"abc",3,MSG_OBB);

那最后的那个字节(字母c)被认为是带外字节。

2.接收端

1)当收到一个设置了URG标志的分节时,接收端TCP检查紧急指针,确定它是否指向新的带外数据。只有第一个到达的会导致通知接收进程有新的分节到达。

(2)当有新的紧急指针达到时,接收进程被通知到。(前提是接收进程调用fcntl或ioctl为这个套接字建立了属主)。只有一个OOB标志,如果新的OOB字节在旧的OOB字节被读取之前就到达,旧的OOB字节会被丢弃。

(3)由紧急指针指向的实际数据字节到达接收端TCP时,该数据既可能被拉出带外,也可能被留在带内,即在线留存。SO_OOBINLINE套接字选项默认情况下是禁止的,接收端套接字把该数据字节放入到该连接的一个独立的单字节带外缓冲区。(唯一接收方法是指定MSG_OOB标志调用recv、recvfrom或recvmsg)

3.开启SO_OOBINLINE套接字选项会发生的一些错误

(1)接收进程请求读入带外数据(通过MSG_OOB标志),但对端尚未发送任何带外数据,读入操作将返回EINVAL。

(2)在接收进程已被告知对端发送了一个带外字节的前提下,如果接收进程试图读入该字节,但是该字节尚未到达,读入操作将返回EWOULDBLOCK。

(3)接收进程试图多次读入同一个带外字节,读入操作将返回EINVAL。

(4)接收进程已经开启了SO_OOBINLINE套接字选项,后来试图通过指定MSG_OOB标志读入带外数据,读入操作将返回EINVAL。

4.sockatmark函数
用sockatmark函数确定是否处于带外标记。

#include <sys/socket.h>
int sockatmark(int sockfd);
/*返回:处于带外标记为1,不处于带外标记为0,出错为-1*/

第26章 线程

1.线程

父子线程的资源共享问题
共享:
全局变量,打开的文件(描述符),文件系统的参数,环境变量。

子线程独特的资源:
线程id,栈空间,信号屏蔽字,errno,信号掩码,优先级。

一、线程
进程里面执行流程的一条。每一个进程至少(默认)有一个线程,被称为主线程。可以在主线程的基础上增加新的线程,为了同时执行多个任务。这种情况叫做多线程。

进程是资源分配的最小单位,线程是执行的最小单位。一个进程里有多个线程。
在linux里面,线程和进程的内部实现是非常接近的,linux里线程是一个轻量级的进程。都是用struct task_struct这个结构体来表示的。

二、线程的编号
如同pid一样,线程也有一个编号,使用pthread_t类型来表示。本质上是一个整型数。
可以使用pthread_self(), 相当于进程的getpid函数。

编译的时候要加上 -pthread这个库。

三、线程的创建

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

第一个参数thread,存储了新线程的id。是一个输出型的参数。
attr是属性,可以在创建的时候提供。可以为NULL。
void *(*start_routine) (void *)是一个指向函数的指针。
线程所需要执行的任务,就是这个函数。
最后一个是传给那个函数的参数。可以为NULL。


线程创建的过程:

使用的都是clone系统调用,只是参数和进程创建不同而已。

四、线程属性的获取和设置
每一个线程都有属性的。相关的设置和获取函数都是以pthread_attr_开头的。
属性有:
使用的栈的地址,大小。
调度方式:
和其它进程(线程)竞争的方式。(有两种,一是和所有的其它进程一起竞争,二是和本进程里的线程竞争)。
属性的数据类型是pthread_attr_t。

五、线程的退出
(1)子线程的执行范围只限制在交给它的函数里。在函数的return的地方退出。

(2)子线程也可以使用类似进程的exit函数,形式是pthread_exit()。
void pthread_exit(void *retval);

(3)其它线程使用pthread_cancel函数结束某个线程。
int pthread_cancel(pthread_t thread);
thread是取消的对象。


六、等待线程
和进程类似,也有等待函数。
int pthread_join(pthread_t thread, void **retval);
thread是等待的对象,retval是对象的函数返回值。
这个函数会阻塞自己等待指定的子线程结束,然后处理它的垃圾。

返回值有如下几种情况:
(1)函数正常返回,则保存的是函数的return 值。
(2)如果使用pthread_exit,则保存exit里的参数。
(3)如果别的线程通过pthread_cancel来结束,则保存的是PTHREAD_CANCELED这个宏。

七、线程的分离
主线程和子线程分离,主线程不再等待子线程,子线程独立运行。
默认创建的子线程是不分离的。如果要分离,有两种途径:
(1)使用pthread_detach
(2)使用pthread_attr_setdetached修改线程属性。
int pthread_detach(pthread_t thread);
thread是分离的对象。

2.线程同步
和进程同步类似,线程同步更多的是保护某个资源对象,让任意时刻只有一个线程访问它。而进程同步是保护某个代码临界区。

互斥锁和条件变量。

1.互斥锁
这种锁只有01两种状态。

相关的操作:
pthread_mutex_t  :数据类型
pthread_mutex_init  :初始化,也可以使用宏的方式初始
pthread_mutex_destroy: 销毁
pthread_mutex_lock:  上锁
pthread_mutex_unlock: 解锁

2.条件变量

相关的操作函数:

pthread_cond_t : 数据类型
pthread_cond_wait: 睡眠等待
pthread_cond_signal:  唤醒等待的线程(单个)
pthread_cond_broadcast: 通知所有等待的线程(多个)

条件变量结合互斥锁可以让主循环进入睡眠,知道某个线程通知它有事可做才醒来。

存钱取钱例子:

#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <signal.h>
//账户
unsigned int account=0;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

int c=0;   //用来触发的
int tiaojian=0;    //条件

void handler(int sig)
{
    if(sig==SIGINT)
    {
        printf("信号发生\n");
        c=1;
    }
}



//线程函数
void *thread_fun(void *arg)
{

    while(1)
    {
        sleep(1);
        //存钱            
        if(c>0)
        {
            pthread_mutex_lock(&mutex);
            tiaojian=1;//先设置条件为真
            pthread_mutex_unlock(&mutex);
            printf("存钱\n");
            pthread_cond_signal(&cond);
            c=0;
        } 

    }
    return NULL;   
}

int main(void)
{
    signal(SIGINT,handler);
    pthread_t thread;
    pthread_create(&thread,NULL,thread_fun,NULL);
    while(1)
    {
        pthread_mutex_lock(&mutex);
        while(tiaojian==0)
            pthread_cond_wait(&cond,&mutex);
       tiaojian=0;
        printf("醒来取钱\n");
        pthread_mutex_unlock(&mutex);
    }

    pthread_join(thread,NULL);
}


“`

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
UNIX网络编程:卷1 套联网API》是一本由W. Richard Stevens所著的经典图书。该书系统地介绍了UNIX操作系统上的套编程技术。 套UNIX网络编程中的核心概念之一,它提供了一种通信机制,使得不同主机间的进程可以进行数据的传输和交换。本书的主要内容包括网络编程基础知识、套编程的基本操作、传输层协议(TCP、UDP)的使用以及网络编程的高级主题,如进程间通信、多线程编程等。 本书共分为24个章节,每个章节都深入浅出地解释了UNIX编程的各个方面。作者通过丰富的示例代码、清晰的图解和详细的解释帮助读者理解并掌握套编程的技巧和实践。 《UNIX网络编程:卷1 套联网API》具有以下特点: 1. 详尽全面:书中对UNIX网络编程的各个方面进行了详细的介绍,从基础知识到高级主题,都有所涉及,对读者来说是一本全面系统的参考书。 2. 实用性强:书中的示例代码贴近实际应用场景,读者可以通过实践演练快速掌握套编程的技能,并了解如何解决实际网络编程中的常见问题。 3. 经典权威:作者W. Richard Stevens是UNIX网络编程领域的权威专家,他在书中融入了自己多年的经验和理论研究成果,使得本书成为了套编程领域的经典之作。 《UNIX网络编程:卷1 套联网API》是一本经典可贵的学习资料,它对UNIX编程提供了系统而丰富的介绍,既适用于初学者入门学习,也适合有经验的开发人员作为参考手册使用。无论是从事网络编程开发的工程师,还是对UNIX网络编程感兴趣的技术爱好者,都会从该书中获得丰厚的知识收益。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值