3.socket 服务器开发步骤及 API 简析

目录

socket 服务器和客户端开发步骤图解 

1.socket (指定讲汉语,连接协议)

2.bind (地址准备好,绑定IP地址和端口号)

端口号- 转换字节序

怎么查询- struct sockaddr_in:

地址转换 API  :

3.listen(监听)

4.accept(连接)

5.数据收发(字节流读取函数)

数据收发常用第二套 API

6.客户端的connect函数

socket服务器--代码demo:

1.(不接收客户端)

2.(接收客户端)完整socket服务器代码

3.(客户端代码)与 第2点的完整socket服务端代码对应。


socket 服务器和客户端开发步骤图解 

 

1.socket (指定讲汉语,连接协议)

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

 如果成功,将返回新套接字的文件描述符。如果出现错误,则返回-1,并适当设置errno。

1.domain:

指明所使用的协议族,通常为 AF_INET ,表示互联网协议族(TCP / IP 协议族);

  • AF_INET      IPv4               因特网域
  • AF_INET6    IPv6               因特网域
  • AF_UNIX      Unix               域 
  • AF_ROUTE                        路由套接字
  • AF_KEY                              密钥套接字
  • AF_UNSPEC                      未指定

2.type参数指定socket的类型:

  • SOCK_STREAM   :

流式套接字提供可靠的,面向连接的通信流,它使用 TCP 协议,从而保证了数据传输的正确性和顺序性

  • SOCK_DGRAM  

数据报套接字定义了一种无连接的符服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠的、无差错的。它使用数据报协议 UDP。

  • SOCK_RAW 

允许程序使用低层协议,原始套接字允许对底层协议如 IP 或 ICMP 进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。

 3.protocol

通常赋值为 “0”。

  • 0 选则 type 类型对应的默认协议
  • IPPROTO_TCP     TCP传输协议
  • IPPROTO_UDP     UDP传输协议
  • IPPROTO_SCTP   SCTP传输协议

2.bind (地址准备好,绑定IP地址和端口号)

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

功能:用于绑定IP地址和端口号到 socketfd

成功,返回0。如果出现错误,则返回-1,并适当设置errno。

  • sockfd

是一个 socket 描述符 

  • addr

是一个指向包含本机 IP 地址及端口号等信息的 sockaddr 类型的指针,指向要绑定给 sockfd 的协议地址结构,这个地址结构根据地址创建 socket 时的地址协议族不同而不同

IPV4 对应的是:

struct sockaddr {
               sa_family_t sa_family;      //协议族
               char        sa_data[14];    //IP+端口
}

同等替换:

struct sockaddr_in {
  sa_family_t           sin_family;     /*协议族*/
  in_port               sin_port;       /*端口号*/
  struct in_addr        sin_addr;       /*IP地址结构体*/
  unsigned char         sin_zero[8]     /*填充  没有实际意义,只是为跟 sockaddr 结构在内存中对齐,这样两者才能互相转换*/

结构体 sockaddr_in -- 参数2 :sin_port   /*端口号*/ 

一般3000以下是系统一些关键端口用的,我们用户一般用5000-9000之间使用的比较多

端口号- 转换字节序

 字节序:https://blog.csdn.net/jinchi_boke/article/details/116253412

怎么查询- struct sockaddr_in:

  1. cd /usr/include                                     (在这一目录下查找)
  2. ls
  3. grep "struct sockaddr_in {" * -nir        (n:找出来显示行号,i:不区分大小写,r:是递归的意思)
  4. vi linux/in.h +184                                  (linux/in.h是头文件)
  5.  

  6. struct sockaddr_in 的第三个参数也是一个结构体 struct in_addr ,

  7. grep "struct in_addr {" * -nir

  8. vi linux/in.h +56

  9.  

地址转换 API  :

int inet_aton(const char *straddr, struct in_addr *addrp);

把字符串形式的"1921.168.1.123"转为网络能识别的格式

char *inet_ntoa(struct in_addr inaddr;

把网络格式的 IP 地址转为字符串形式。
 

  • addrlen

也就是第二个参数的结构体的大小。

3.listen(监听)

int listen(int sockfd, int backlog);

1.功能

  • 设置能处理的最大连接数,llisten( ) 并未开始接受连线,只是设置socket 的 listen 模式,listen 函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被套接字(监听),规定内核为相应套接字排队的最大连接数。
  • 内核为任何一个给定监听套接字维护两个队列:
  1. 未完成连接队列,每个这样的 SYN 报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接字处于 SYN_REVD 状态。
  2. 已完成连接队列,每个已完成 TCP 三次握手过程的客户端对应其中一项。这些套接字处于 ESTABLISHED 状态。

2.参数

  • sockfd

sockfd 是 socket 系统调用返回的服务器端 socket 描述符

  • backlog

backlog 指定在请求队列中允许的最大请求数(监听几个)

4.accept(连接)

 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

如果成功,这些系统调用将返回一个非负整数,它是所接受的套接字的描述符。如果出现错误,则返回-1,并适当设置errno。

功能:

  • accept 函数由 TCP 服务器调用,用于从已完成连接队列列头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。

参数:

  • sockfd

sockfd 是 socket 系统调用返回的服务器 socket 描述符

  • addr

用来返回已连接的对端(客户端)的协议地址

  • addrled

客户端地址长度

返回值:

  • 该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅床架一个监听套接字,它在该服务器的声明周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示 TCP 三次握手已完成),放服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。

5.数据收发(字节流读取函数)

 在套接字 通信中进行字节读取函数:read( ),write( )。与 I/O 中的读取函数略有区别,因为它们输入或输出的字节数比可能比请求的少。

ssize_t write(int fd, const void *buf, size_t nbytes);
ssize_t read(int fd, void *buf, size_t nbytes);

说明:
函数均返回读或写的字节个数,出错则返回 -1

第一个将 buf 中的 nbytes 个字节写入到文件描述符 fd 中,成功时返回写的字节数。第二个为从 fd 中读取 nbytes 个字节到 buf 中,则返回实际所读的字节数。详细应用说明参考使用 read write 读写 socket (套接字)。

网络 I/O还有一些函数,例如: recv()/send() ,readc()/write(),recvmsg()/sendmsg(),recvfrom()/sendti() 等

数据收发常用第二套 API

  1. 在 TCP 套接字上发送数据函数:有连接
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

 包含2要素:套接字sockfd,待发数据 buf,数据长度 len

函数只能对处于连接状态的套接字使用,参数 sockfd 为已建立好连接的套接字描述符,即是accpet函数的返回值

参数 buf 指向存放待发送数据的缓冲区

参数 len 为待发送数据的长度,参数flags 为控制选项,一般设置为 0 ;

2.在 TCP 套接字上接收数据函数:有连接

 ssize_t recv(int sockfd, void *buf, size_t len, int flags);

  包含2要素:套接字sockfd,待发数据 buf,数据长度 len

函数 recv 从参数 sockfd 所指定的套接字描述符(必须是面向连接的套接字)上接收

数据并保存到参数 buf 所指定的缓冲区

参数 len 则为缓冲区长度,参数 flags 为控制选项,一般设置为 0 ;

6.客户端的connect函数

connect()函数:客户机连接主机

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

 功能:

该函数用于绑定之后的 client 端(客户端),与服务器建立连接

参数:

  • sockfd

是目的服务器的sockect 描述符

  • addr

是服务器端的 IP 地址和端口号的地址结构指针

  • addrlen 

地址长度常被设置为 sizeof(struct sockaddr)

返回值:

成功返回0,遇到错误时返回 -1 ,并且errno 中包含相应的错误码

socket服务器--代码demo:

1.(不接收客户端)

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>

int main()
{
        int s_fd;

        //1.socket
        s_fd = socket(AF_INET,SOCK_STREAM,0);
        if(s_fd == -1){
                perror("socked");
                exit(-1);
        }

        struct sockaddr_in s_addr;
        s_addr.sin_family = AF_INET;
        s_addr.sin_port = htons(8989);
        //s_addr.sin_addr.s_addr = "127.0.0.1";  (error)
        inet_aton("127.20.10.10",&s_addr.sin_addr);        //写虚拟机linux的ip地址,用于等会连接

        //2.bind
        bind(s_fd, (struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));

        //3.listen
        listen(s_fd,10);

        //4.accept
        int c_fd = accept(s_fd,NULL,NULL);

        //5.read

        //6.write
        printf("connect\n");
        while(1);

        return 0;
}

运行代码,用 win10 的cmd 来用 telnet 命令连接:  telnet 172.20.10.10 8989  (此时相当于两台电脑在相连)

telnet 是 TCP 协议的

2.(接收客户端)完整socket服务器代码

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>

//int socket(int domain, int type, int protocol);
//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

int main()
{
        int s_fd;
        int n_read;
        char readBuf[128];

        char *msg = "I got your message";
        struct sockaddr_in s_addr;
        struct sockaddr_in c_addr;

        memset(&s_addr,0,sizeof(struct sockaddr_in));    //一般来说先清空空间数据,再配置。避免结构体里面有杂乱数据
        memset(&c_addr,0,sizeof(struct sockaddr_in));

        //1.socket
        s_fd = socket(AF_INET,SOCK_STREAM,0);
        if(s_fd == -1){
                perror("socked");
                exit(-1);
        }

        s_addr.sin_family = AF_INET;
        s_addr.sin_port = htons(8988);
        //s_addr.sin_addr.s_addr = "127.0.0.1";  (不是这样写的)
        inet_aton("127.0.0.1",&s_addr.sin_addr);

        //2.bind
        bind(s_fd, (struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));

        //3.listen
        listen(s_fd,10);

        //4.accept
        int clen = sizeof(struct sockaddr_in);
        int c_fd = accept(s_fd,(struct sockaddr *)&c_addr, &clen);
        if(c_fd == -1){
                perror("accept");
        }
        printf("get connect:%s\n",inet_ntoa(c_addr.sin_addr));    //inet_ntoa()函数,把网络格式的 IP 地址转为字符串形式

        //5.read
        n_read = read(c_fd,readBuf,128);
        if(n_read == -1){
                perror("read");
        }else{
                printf("get message: %d ,%s\n",n_read,readBuf);
        }

        //6.write
        write(c_fd,msg,strlen(msg));

        return 0;
}

3.(客户端代码)与 第2点的完整socket服务端代码对应。

int main()
{
        int c_fd;
        int n_read;
        char readBuf[128];

        char *msg = "Message form client";
        struct sockaddr_in c_addr;

        memset(&c_addr,0,sizeof(struct sockaddr_in));

        //1.socket
        c_fd = socket(AF_INET,SOCK_STREAM,0);
        if(c_fd == -1){
                perror("socked");
                exit(-1);
        }

        c_addr.sin_family = AF_INET;
        c_addr.sin_port = htons(8988);
        //s_addr.sin_addr.s_addr = "127.0.0.1";  //(不是这样写的)
        inet_aton("127.0.0.1",&c_addr.sin_addr);

        //2.connect
        if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in)) == -1){
                perror("connect");
                exit(-1);            //连接不到服务端会阻塞,出错直接跳出程序,以免搞崩代码
        }

        //3.send
        write(c_fd,msg,128);

        //4.read
        n_read = read(c_fd,readBuf,128);
        if(n_read == -1){
                perror("read");
        }else{
                printf("get message from server: %d ,%s\n",n_read,readBuf);
        }

        return 0;
}

遇到问题:

在 客户端连接协议 connect( ) 前  把声明的 struct sockaddr_in c_addr 给清空了 ,如下

memset(&c_addr,0,sizeof(struct sockaddr_in));

if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in)) == -1){
                perror("connect");
                exit(-1);            //连接不到服务端会阻塞,出错直接跳出程序,以免搞崩代码
        }

 此时运行之后一直和服务端连接不上。

原因:主要就是自己编程思路不够严谨,因为 memset()是把数据给清空了,所以导致connect( ) 没有数据的连接。

正确的做法是将 清空数据的memset( )  随手就放到声明的变量之后。

这样一来:可以先清空数据再赋值,避免本身杂乱的数据影响。

二来:就少了一个编程思维不严谨导致的一系列不明所以。

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

枕上

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值