第2关:socket编程

任务描述

本关任务:
编写两个程序实现客户端与服务端的通信,clinet.c为客户端,server.c为服务器端。服务器端能够读取客户端发送的信息。

相关知识

为了完成本关任务,你需要掌握:1.socket编程,2.数据通信过程。

1、Socket

  网络中进程可以通过socket通信,socket起源于Unix,满足“一切皆文件”原理,即操作为“打开open->读写write/read->关闭close”。
  ①socket的调用函数主要有:

    socket()/*创建描述符,设定协议域和socket类型*/
    bind()/*绑定地址*/
    listen()/*监听*/
    connect()/*连接*/
    read()/*I/O读操作*/
    write()/*I/O写操作*/
    close()/*断开连接*/
    shutdown()/*部分断连*/

 ②socket操作主要有三类:
 1)三次握手:让客户端与服务端双方都能明确自己和对方的收、发能力是正常的。
 2)数据传输:socket使用的是TCP连接,为双向传输的对等模式,双方都可以同时向对方发送或接收数据。
 3)四次挥手:由于TCP连接是全双工,每个方向都必须单独进行关闭。

2、socket函数

int socket(int domain,int type,int protocol)
sockfd=socket();
    socket()创建一个socket描述符,标识唯一一个socket
    domain:协议域->AF_INET、AF_INET6、AF_LOCAL(AF_UNIX)、AF_ROUTE。协议族决定socket地址类型,通信中必须采用对应的地址,AF_INET决定要用ipv4地址(32位)与端口号(16位)组合看,AF_UNIX决定用一个绝对路径作为地址。
    type:socket->指定socket类型。常用SOCK_STREAM(TCP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET、SOCK_DGRAM(UDP)等
    protocol:协议->常用协议IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输、UDP传输、STCP传输、TIPC传输

注:type和protocol不可用随意组合,SOCKET_STREAM和IPPROTO_UDP不可用组合。当protocol为0,会自动选择type类型对应默认协议。
  socket函数创建一个socket时,返回的socket描述符存在于协议族address family中,没有具体地址,赋值一个地址必须调用bind()或者调用connect()、listen()系统自动随机分配端口。

3、bind函数

 bind()函数把一个地址族中特定地址赋值给socket,AF_INET、AF_INET6就是一个ipv4或ipv6地址和端口号组合,赋值给socket。

int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
    sockfd:socket描述符,通过socket()创建,唯一标识一个socket。bind()将这个描述符绑定一个名字。
    addr:const struct sockaddr*指针,指向sockfd的协议地址。地址结构根据创建socket时的地址协议族不同而变化。
    addrlen:地址长度

注:①服务器启动时会绑定地址(ip+端口号)。提供服务时,服务端通过地址连接服务器。客户端不用绑定,系统自动分配端口号和自身iP地址组合。所以通常服务端listen前会调用bind(),客户端不会调用,在connect()时系统随机生成一个。
  ②主机字节序(大端小端模式):不同CPU有不同字节序类型,这些字节序指整数在内存中保存的顺序。
  1)little-endian,低位字节放内存低地址段,高位字节排放内存高地址端。
   2)big-endian,高位字节放内存低地址段,低位字节放内存高地址端。
  网络字节序:4个字节的32bit值以下次序传输,0-7、8-15、16-23、24-31。大端字节序。由于TCP/IP首部中所有二进制整数在网络中传输时都要求以这种次序,因此又称作网络字节序。字节序,发育一个字节类型数据在内存中存放顺序,一个字节的数据没有顺序问题。
  在绑定地址到socket时候,必须将主机字节序转换成网络字节序,不能让主机字节序跟网络字节序一样使用big-endian。

4、listen、connetct函数

 服务器调用socket()、bind()后就会调用listen()监听socket,如果客户端这时候调用connect()发出连接请求,服务器就会接受这个请求。

int listen(int sockfd,int backlog);
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
    listen中sockfd为要监听的socket描述符,backlog为相应socket可以排队的最大连接个数。
    socket()函数创建的socket是一个主动类型,listen()将socket变为被动类型等待客户连接请求。
    connect中sockfd为客户端socket描述符,addr为服务器socket地址,addrlen为socket地址长度。客户端调用connect函数建立与TCP服务器的连接。

5、accept函数

 TCP服务器调用socket()、bind()、listen()后,监听socket地址。TCP客户端调用socket()、connect()后,给TCP服务器发送一个连接请求。TCP服务器监听到请求,调用accept()接收请求,连接建立好开始I/O操作。

int accept(int sockfd,struct sockaddr *addr,socklen_t addrlen)
    sockfd服务器socket描述符
    addr返回客户端协议地址
    addrlen协议地址长度

 accept成功,返回值由内核自动生成的全新描述符,表示已与返回客户的TCP连接。
注:sockfd是服务器的socket描述符,服务器开始调用socket()生成的,即监听socket描述符,accept返回的是已连接socket描述符,一个服务器通常只创建一个监听socket描述符,在服务器生命周期内会一直存在,内核为每个服务器进程接收的客户连接创建一个已连接socket描述符,当服务器完成客户服务,相应已连接socket描述符会被关闭。

6、read、write函数

 服务器与客户建立好连接,调用网络I/O进行读写操作,网络中不同进程之间通信。

网络I/O操作
    read()/write()
    recv()/send()
    readv()/writev()
    recvmsg()/sendmsg()
    recvfrom/sendto()

7、close、shutdown函数

 服务器与客户端建立连接后,读写操作完,需要关闭相应的socket描述符。

#include<unistd.h>
int close(int fd);

 sockfd不能在作为read或者write的第一个参数。
注:close操作只是将相应的sockfd描述字引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

 shutdown()函数,可以关闭socket一端或者全部。

int shutdown(int _fd,int _how)
    TCP连接是双向的(可读可写),使用close读写通道都关闭,使用shutdown有三种:
    ①howto=0,关闭读通道,可以继续写。
    ②howto=1,关闭写通道,只可以读。
    ③howto=2,关闭读写通道,和close一样,全部关闭。

8、三次握手

tcp建立连接进行“三次握手”:
①客户端向服务器发送一个SYN x。
②服务器向客户端发送一个SYN y,并对SYN x进行ACK x+1。
③客户端再向服务器发一个ACK y+1。
,

 ①客户端调用connect触发连接请求,向服务器发送SYN x包,connect进入阻塞状态;
 ②服务器listen监听到连接请求(收到SYN x),调用accept接受请求,向客户端发送SYN y和ACK x+1,accept进入阻塞状态;
 ③客户端收到服务器的SYN y和ACK x+1,对SYN y确认,connect返回ACK y+1;
 ④服务器收到ACK y+1,accept返回,三次握手完毕,连接建立。
注:客户端的connect在三次握手的第二次握手返回,服务端的accept在三次握手的第三次握手返回。

9、数据传输

,

①建立完连接后,进行数据传输,read和write。
②客户端发起给服务端写入,并发送SYN x+1和ACK y+1给服务端(xy未发生变化)。
③服务端发起给客户端读取,并发送ACK x+2。

10、四次挥手

,

①客户端应用进程调用close主动关闭连接,客户端TCP发送FIN x+2和ACK y+1。
②服务端接受到FIN x+2,执行被动关闭,对FIN确认。即当服务端收到FIN后停止数据的操作并发出一个ACK x+3客户端表示数据操作结束。
③一段时间后,服务端应用接收到文件结束符的应用进程调用close关闭服务端socket,发送一个FIN。(表示socket已经关闭)
④客户端接收到FIN,关闭客户端的socket,发送ACK y+2高速服务端。

编程要求及注意事项

根据client.c代码及提示,在右侧编辑器中选中server.c注释处补充代码。

测试说明

平台会对你编写的代码进行测试,比对输出结果。


开始你的任务吧,祝你成功!

参考代码

/*server*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 4096

int main(int argc, char *argv[])
{
    
    
    int listenfd, connfd;        // listenfd是监听描述字,connfd是accept的连接描述字
    struct sockaddr_in servaddr; //创建servaddr记录服务器端的地址
    char buff[MAXLINE];          // buff缓存用作缓存从client发来的数据
    int recv_n;                  // recv_n用来记录传输数据大小

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
    
    
        printf("create socket error:%s(errno:%d)\n", strerror(errno), errno);
        exit(0);
    } //创建原始监听描述字listenfd,用的是AF_INET ipv4协议簇,SOCK_STREAM是TCP socket类型,失败返回-1

    memset(&servaddr, 0, sizeof(servaddr));       //将服务端地址
    servaddr.sin_family = AF_INET;                //设置服务端地址的协议簇为ipv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //设置服务端接收的访问地址是INADDR_ANY,即所有ip都可以访问服务端
    servaddr.sin_port = htons(6666);              //服务器的端口号是6666
                                                  /* 服务端需要提前设置好服务端地址,给客户端访问时用*/

    /***************************************************************************
     * 1、实现把监听描述字绑到设置好的服务端地址,注意第二个参数是地址,需要类型强转
     **************************************************************************/
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
    
    
        printf("bind socket error:%s(errno:%d)\n", strerror(errno), errno);
        exit(0);
    }

    /*************************************************************************
     * 2、实现监听描述符listenfd开始监听,最大监听排队数10
     *************************************************************************/
    if (listen(listenfd, 10) == -1)
    {
    
    
        printf("listen socket error:%s(errno:%d)\n", strerror(errno), errno);
        exit(0);
    }

    printf("=================waiting for client's request===============\n");

    while (1)
    {
    
    
        /*************************************************************************
         * 3、实现当收到申请时,为新申请创建连接,新的connfd描述符
         *************************************************************************/
        int addrlen = sizeof(servaddr);
        if ((connfd = accept(listenfd, (struct sockaddr *)&servaddr, &addrlen)) == -1)
        {
    
    
            printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);
            continue;
        }

        recv_n = recv(connfd, buff, MAXLINE, 0); //收到来自connfd连接的字符,存入buffer中,返回串长度recv_n
        buff[recv_n] = '\0';                     //将最后一个接收字符后加入\0,后面输出到\0就会停

        printf("recv msg from client:%s\n", buff); //输出buff中的字符串
        fflush(stdout);                            //刷新缓冲区
        close(connfd);                             //关闭连接描述字connfd(四次挥手的第二次挥手)
    }
    close(listenfd); //关闭所有监听,服务端停止通信(四次挥手的第三次挥手)
}

猜你喜欢

转载自blog.csdn.net/qq_46373141/article/details/130981880