Linux网络编程(2)

1、客户端程序

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666
int main()
{
        int cfd;
        struct sockaddr_in serv_addr;
        socklen_t serv_addr_len;
        char buf[BUFSIZ];
        cfd = socket(AF_INET,SOCK_STREAM,0);
        int n;

        memset(&serv_addr,0,sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(SERV_PORT);
        inet_pton(AF_INET,SERV_IP,&serv_addr.sin_addr.s_addr);

        connect(cfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
        while(1)
        {
                fgets(buf,sizeof(buf),stdin);
                write(cfd,buf,strlen(buf));
                n = read(cfd,buf,sizeof(buf));
                write(STDOUT_FILENO,buf,n);
        };
        close (cfd);
}
fgets()函数,键入hello world  fgets读取到的是 "hello world\n\0"
stdio获取用户输入

2、错误处理函数
    netstat -apn | grep 6666 查看端口号
    先关server 再关client 再启动的时候就会出错
    
    封装出错处理函数和头文件
    要点就是把系统函数封装成用户自己的函数,把首个字母变大写(方便进入man手册),在函数内部调用系统函数。代码见/root/Liunx/3/1Days wrap.c和wrap.h

wrap.c:

#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s)
{
        perror(s);
        exit(1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
        int n;
        again:
        if ( (n = accept(fd, sa, salenptr)) < 0) {
                if ((errno == ECONNABORTED) || (errno == EINTR))
                        goto again;
                else
                        perr_exit("accept error");
        }
        return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
        int n;
        if ((n = bind(fd, sa, salen)) < 0)
                perr_exit("bind error");
        return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
        int n;
        if ((n = connect(fd, sa, salen)) < 0)
                perr_exit("connect error");
        return n;
}
int Listen(int fd, int backlog)
{
        int n;
        if ((n = listen(fd, backlog)) < 0)
                perr_exit("listen error");
        return n;
}
int Socket(int family, int type, int protocol)
{
        int n;
        if ( (n = socket(family, type, protocol)) < 0)
                perr_exit("socket error");
        return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
        ssize_t n;
again:
        if ( (n = read(fd, ptr, nbytes)) == -1) {
                if (errno == EINTR)
                        goto again;
                else
                        return -1;
        }
        return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
        ssize_t n;
again:
        if ( (n = write(fd, ptr, nbytes)) == -1) {
                if (errno == EINTR)
                        goto again;
                else
                        return -1;
        }
        return n;
}
int Close(int fd)
{
        int n;
        if ((n = close(fd)) == -1)
                perr_exit("close error");
        return n;
}
ssize_t Readn(int fd, void *vptr, size_t n)
{
        size_t nleft;
        ssize_t nread;
        char *ptr;

        ptr = vptr;
        nleft = n;

        while (nleft > 0) {
                if ( (nread = read(fd, ptr, nleft)) < 0) {
                        if (errno == EINTR)
                                nread = 0;
                        else
                                return -1;
                } else if (nread == 0)
                        break;
                nleft -= nread;
                ptr += nread;
        }
        return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
        size_t nleft;
        ssize_t nwritten;
        const char *ptr;

        ptr = vptr;
        nleft = n;

        while (nleft > 0) {
                if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
                        if (nwritten < 0 && errno == EINTR)
                                nwritten = 0;
                        else
                                return -1;
                }
                nleft -= nwritten;
                ptr += nwritten;
        }
        return n;
}

static ssize_t my_read(int fd, char *ptr)
{
        static int read_cnt;
        static char *read_ptr;
        static char read_buf[100];

        if (read_cnt <= 0) {
again:
                if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
                        if (errno == EINTR)
                                goto again;
                        return -1;
                } else if (read_cnt == 0)
                        return 0;
                read_ptr = read_buf;
        }
        read_cnt--;
        *ptr = *read_ptr++;
        return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
        ssize_t n, rc;
        char c, *ptr;
        ptr = vptr;

        for (n = 1; n < maxlen; n++) {
                if ( (rc = my_read(fd, &c)) == 1) {
                        *ptr++ = c;
                        if (c == '\n')
                                break;
                } else if (rc == 0) {
                        *ptr = 0;
                        return n - 1;
                } else
                        return -1;
        }
        *ptr = 0;
        retrun n;
}

wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif

3、read返回值总结
    accept 有一个宏ECONNABORTED,被信号终端,不算错误,goto again 重启一下
    read函数返回值
    >0 实际读到的字节数
    =0数据写完(读到文件、管道、socket 末尾--对端关闭)
    -1异常
    error = EINTR 被信号中断
    error == EAGAIN(EWOULDBLOCK)以非阻塞方式读,并且没有数据
    其他值:出现错误

4、read和readline函数封装
    readn:读取n个字节,通过函数可以知道读了多少字节还有多少没读。

    在socket中,read一次之讷讷过度1500字节,如果我们要读一个4096的数据包,就需要用这个函数,就可以知道还有多少没读。

my_read 通过传出参数一次读出100字节

readline 在socket中不能使用fgets,所以封装一个readline函数来替代fgets

 

5、TCP三次握手
    网络层的IP有不稳定性。与硬件紧密连接
    传输层有两种方式
        1、对这种不稳定 采用完全不弥补 -- UDP 无连接的不可靠报文传输
        2、完全弥补,发送数据报会有记录,没有接受到重发,就是tcp,面向连接的可靠的数据报传递
    TCP如何保证呢:
        面向连接:数据报传递之前,在发送端和接收端建立一个通路,用TCP发送数据他的通路是相同的,建立通路过程就是TCP三次握手,解除链接就是四次握手

 

在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-10,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK1001, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss(Maximum Segment Size,最大报文长度)选项值为1024。

建立连接(三次握手)的过程:

  1. 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1。

客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。

     2、服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。

服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。

     3、客户必须再次回应服务器端一个ACK报文,这是报文段3。

客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。

在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection refused:

$ telnet 192.168.0.200 8080

Trying 192.168.0.200...

telnet: Unable to connect to remote host: Connection refused

数据传输的过程:

  1. 客户端发出段4,包含从序号1001开始的20个字节数据。
  2. 服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。
  3. 客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。

在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。

关闭连接(四次握手)的过程:

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. 客户端发出段7,FIN位表示关闭连接的请求。
  2. 服务器发出段8,应答客户端的关闭连接请求。
  3. 服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。
  4. 客户端发出段10,应答服务器的关闭连接请求。

建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。        

    TCP丢了包会重发,UDP不会重发

    TCP滑动窗口

作用:流量控制,防止数据包发送过快,处理过慢,造成数据丢失

发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过“滑动窗口(Sliding Window)”机制解决这一问题。看下图的通讯过程:

 

     

  1. 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
  2. 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
  3. 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
  4. 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
  5. 发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
  6. 接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
  7. 接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
  8. 接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
  9. 接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。

上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。

从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

MSS:maximum segment size,最大分节大小,为TCP数据包每次传输的最大数据分段大小,一般由发送端向对端TCP通知对端在每个分节中能发送的最大TCP数据。MSS值为MTU值减去IPv4 Header(20 Byte)和TCP header(20 Byte)得到。

分片:若一IP数据报大小超过相应链路的MTU的时候,IPV4和IPV6都执行分片(fragmentation),各片段到达目的地前通常不会被重组(re-assembling)。IPV4主机对其产生的数据报执行分片,IPV4路由器对其转发的数据也执行分片。然而IPV6只在数据产生的主机执行分片;IPV6路由器对其转发的数据不执行分片。

win 窗口大小(表示客户端上用来存储从服务器发送来的传入段的缓冲区的大小)。


    半关闭,一方断开,一方仍然建立连接,连接这段还可以发送,关闭端也可以接受,但不可发送
    1000和1001在TCP的序号的确认序号 建立连接的标志位在TCP数据包的标志位中

6、协议上限分析     
    IP:上限65536
    以太网最多1500
    一个数据包如果有60000,就把这个包拆分开,分几个包发送

7、多进程并发服务器
    一个service面对多个client时,可以建立几个子进程,让子进程去接收处理别的client,父进程去揽活

 

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<strings.h>
#include<sys/wait.h>
#include"wrap.h"
#define SERV_PORT 8888
//#define SERV_IP "192.668.42.100"


void wait_child(int singo)
{
        while(waitpid(0,NULL,WNOHANG)>0);
        return ;
}


int main(void)
{
        int lfd,cfd;
        struct sockaddr_in serv_addr,clieaddr;
        socklen_t clie_addr_len;
        lfd = Socket(AF_INET,SOCK_STREAM,0);
        pid_t pid;
        char buf[BUFSIZ],clie_IP[BUFSIZ];
        int n,i;

        bzero(&serv_addr,sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(SERV_PORT);
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        //inet_pton(AF_INET,"192.168.1.100",serv_addr.sin_addr.s_addr);
        Bind(lfd,(struct socketaddr *)&serv_addr,sizeof(serv_addr));

        Listen(lfd,128);

        while(1)
        {
                clie_addr_len = sizeof(clieaddr);
                cfd = Accept(lfd,(struct sockaddr *)&clieaddr,&clie_addr_len);
                printf("client IP:%s,port:%d\n",inet_ntop(AF_INET,&clieaddr.sin_addr.s_addr,clie_IP,sizeof(clie_IP)),ntohs(clieaddr.sin_port));
                pid = fork();
                if(pid <0)
                {
                        perror("fork error");
                        exit(1);
                }
                else if(pid == 0)
                {
                        close(lfd);
                        break;
                }
                else
                {
                        close(cfd);
                        signal(SIGCHLD,wait_child);
                }
        }
        if(pid == 0)
        {
                while(1)
                {
                        n = Read(cfd,buf,sizeof(buf));
                        if(n == 0)
                        {
                                close(cfd);
                                return 0;
                        }
                        else if(n == -1)
                        {
                                perror("read error");
                                exit(1);
                        }
                        else
                        {
                                for(i =0;i<n;i++)
                                {
                                        buf[i] = toupper(buf[i]);
                                }
                                write(cfd,buf,n);
                        }
                }
     

多线程实现

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>

#include "wrap.h"

#define MAXLINE 8192
#define SERV_PORT 8000

struct s_info {                     //定义一个结构体, 将地址结构跟cfd捆绑
    struct sockaddr_in cliaddr;
    int connfd;
};

void *do_work(void *arg)
{
    int n,i;
    struct s_info *ts = (struct s_info*)arg;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];      //#define INET_ADDRSTRLEN 16  可用"[+d"查看

    while (1) {
        n = Read(ts->connfd, buf, MAXLINE);                     //读客户端
        if (n == 0) {
            printf("the client %d closed...\n", ts->connfd);
            break;                                              //跳出循环,关闭cfd
        }
        printf("received from %s at PORT %d\n",
                inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
                ntohs((*ts).cliaddr.sin_port));                 //打印客户端信息(IP/PORT)

        for (i = 0; i < n; i++) 
            buf[i] = toupper(buf[i]);                           //小写-->大写

        Write(STDOUT_FILENO, buf, n);                           //写出至屏幕
        Write(ts->connfd, buf, n);                              //回写给客户端
    }
    Close(ts->connfd);

    return (void *)0;
}

int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    pthread_t tid;
    struct s_info ts[256];      //根据最大线程数创建结构体数组.
    int i = 0;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);                     //创建一个socket, 得到lfd

    bzero(&servaddr, sizeof(servaddr));                             //地址结构清零
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);                   //指定本地任意IP
    servaddr.sin_port = htons(SERV_PORT);                           //指定端口号 8000

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //绑定

    Listen(listenfd, 128);      //设置同一时刻链接服务器上限数

    printf("Accepting client connect ...\n");

    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);   //阻塞监听客户端链接请求
        ts[i].cliaddr = cliaddr;
        ts[i].connfd = connfd;

        /* 达到线程最大数时,pthread_create出错处理, 增加服务器稳定性 */
        pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
        pthread_detach(tid);                                                    //子线程分离,防止僵线程产生.
        i++;
    }

    return 0;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值