Linux网络编程 Part2 Socket编程

预备知识

网络套接字socket

属于Linux特殊文件类型(管道、套接字、字符设备、块设备)

一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)

在通信过程中, 套接字一定是成对出现的

类比前面进程通信中的管道:一个管道掌控着两个文件描述符和一个缓冲区,而一个套接字掌控着一个文件描述符和两个缓冲区

ps:图中画的不恰当,数据流通并不是从一个套接字的发送端到另一个套接字的接收端,而是从一个套接字的文件描述符流向另一个套接字的文件描述符

网络字节序

小端法:(pc本地存储) 高位存高地址,低位存低地址

大端法:(网络存储) 高位存低地址,低位存高地址(更符合人类阅读习惯,比如数字高位在前)

因此为使网络程序具有可移植性,要调用以下函数做网络字节序和主机字节序的转换:

htonl:本地→网络(针对IP)转换过程:192.168.1.11→string→atoi→int→htonl→网络字节序

htons:本地→网络 (port)

ntohl:网络→本地(IP)

ntohs:网络→本地(Port)

ps:h代表host、n代表network、l代表32位长int用于IP、s代表16位短int用于端口、函数名中间是to

IP地址转换函数

通过封装的函数直接实现针对IP地址的字节序间的转换

int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP)→网络字节序,一般用于客户端绑定服务器的时候

参数:

  • af:AF_INET(指IPv4)、AF_INET6(指IPv6)
  • src:传入参数,IP地址(点分十进制)
  • dst:传出参数,转换后的网络字节序的IP地址

返回值:

  • 成功: 1
  • 失败:-1,errno
  • 异常: 0(说明src指向的不是一个有效的IP地址)

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序→本地字节序(string IP),一般用于accept函数

参数:

  • af:AF_INET(指IPv4)、AF_INET6(指IPv6)
  • src:传入参数,网络字节序IP地址
  • dst:传出参数,转换后的本地字节序(string IP)
  • size:缓冲区dst的大小

返回值:

  • 成功:dst
  • 失败:NULL

sockaddr地址结构

IP + port → 在网络环境中唯一标识一个进程

ps:查询文档 man 7 ip

struct sockaddr_in addr; //定义现在有效的类型  
//初始化sockaddr结构体
addr.sin_family = AF_INET; //指定地址族(IPv4或6)
addr.sin_port = htons(9527); //指定端口号(转化为网络字节序)

//对第三个成员初始化有下列两种方式
①指定IP地址(一般用于客户端)
inet_pton(AF_INET, "192.157.22.45", &addr.sin_addr.s_addr);
②自动取出系统中有效的任意IP地址(一般用于服务器)
addr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(fd, (struct sockaddr *)&addr, size); //调用函数时再强转为函数所需类型(因为函数很老了)

※read函数返回值讨论

  • >0:实际读到的字节数
  • = 0:(表示对端先关闭连接)已经读到结尾,可以调close()函数了
  • -1:应进一步判断error的值
    • errno == EAGAIN或EWOULDBLOCK:设置了非阻塞方式读,但没有数据到达(需要再次读)
    • errno == EINTR:慢速系统调用被中断(需要重启)
    • errno == ECONNRESET:连接被重置(需要close(),移除监听队列)
    • errno == 其他情况:异常

网络套接字函数

socket模型创建流程图

一个客户端和一个服务器进行通信,需要创建三个套接字

其中两个属于服务器(一个用于监听,一个用于通信),一个属于客户端(用于通信)

socket函数

头文件:#include<sys/socket.h>

int socket(int domain, int type, int protocol); //创建一个套接字

参数:

  • domain:AF_INET、AF_INET6、AF_UNIX //指定选用的IP地址协议
  • type:SOCK_STREAM、SOCK_DGRAM //指定数据传输协议:流式协议/报式协议
  • protocol:0 //表示所选择的传输协议的代表协议,选0就代表根据前面指定的传输协议自动选择其代表协议(流式协议的代表协议是TCP,报式协议的代表协议是UDP)

返回值:

  • 成功:新套接字对应的文件描述符
  • 失败: -1,errno

bind函数

头文件:#include<arpa/inet.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个地址结构(IP+port)

参数:

  • sockfd:套接字对应的文件描述符,即socket函数返回值
  • addr:传入参数(struct sockaddr *)&addr 地址结构,注意要强转为struct sockaddr *类型
    • struct sockaddr_in addr;
    • addr.sin_family = AF_INET;
    • addr.sin_port = htons(8888);
    • addr.sin_addr.s_addr = htonl(INADDR_ANY);
  • addrlen:sizeof(addr) 地址结构的大小

返回值:

  • 成功:0
  • 失败:-1,errno

listen函数

int listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数(即同时进行3次握手的客户端数量)

参数:

  • sockfd:套接字对应的文件描述符,即socket函数返回值
  • backlog:上限数值(最大值为128)

返回值:

  • 成功:0
  • 失败:-1,errno

accept函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功则返回一个与客户端成功连接的服务器的socket文件描述符

参数:

  • sockfd:套接字对应的文件描述符,即socket函数返回值
  • addr:传出参数,成功与服务器建立连接用于通信的那个客户端的地址结构(IP+port)
  • addrlen:传入传出参数
    • 入:addr的大小
    • 出:客户端addr的实际大小

返回值:

  • 成功:能与客户端进行数据通信的socket对应的文件描述符
  • 失败:-1,errno

connect函数

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的socket与服务器建立连接

参数:

  • sockfd:套接字对应的文件描述符,即socket函数返回值
  • struct sockaddr_in srv_addr; 传入参数,服务器的地址结构
    • srv_addr.sin_family = AF_INET;
    • srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致
    • inet_pton(AF_INET, “服务器的IP地址”,&srv_adrr.sin_addr.s_addr);
  • addrlen:服务器的地址结构大小

返回值:

  • 成功:0
  • 失败:-1,errno

ps:如果不使用bind函数绑定客户端地址结构,则默认采用”隐式绑定”


C/S模型-TCP

TCP通信流程分析

服务器server:

  1. socket() 创建socket
  2. bind() 绑定服务器地址结构
  3. listen() 设置监听上限
  4. accept() 阻塞监听客户端连接
  5. read(fd) 读socket获取客户端数据
  6. toupper() 小→大写(即具体处理事务)
  7. write(fd) 再由同一个socket向客户端写
  8. close();

客户端client:

  1. socket() 创建socket
  2. connect(); 与服务器建立连接
  3. write() 写数据到socket
  4. read() 读处理后的数据
  5. 处理读取数据
  6. close()

※编程练习:实现TCP通信

server:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>
#include<sys/socket.h>
#include<ctype.h>
#include<arpa/inet.h>

#include"wrap.h"

#define SERV_PORT 8000

int main(int argc, char *argv[]) {
    char buf[BUFSIZ];
    int listen_fd, connect_fd;
    socklen_t clit_addr_len;
    struct sockaddr_in serv_addr, clit_addr;
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    listen_fd = Socket(AF_INET, SOCK_STREAM, 0); //调用自封装函数,将代码逻辑与出错处理解耦

    Bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    Listen(listen_fd, 128);

    clit_addr_len = sizeof(clit_addr);
    connect_fd = Accept(listen_fd, (struct sockaddr *)&clit_addr, &clit_addr_len);

    //输出查看客户端地址结构
    char client_ip[1024];
    printf("client ip:%s port:%d\n",
                    inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
                    ntohs(clit_addr.sin_port));

    int i;
    while(1) {
        int n = Read(connect_fd, buf, sizeof(buf));
        for(i = 0; i < n; i++) buf[i] = toupper(buf[i]);
        Write(connect_fd, buf, n);
    }

    close(listen_fd);
    close(connect_fd);

    return 0;
}

client:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>
#include<sys/socket.h>
#include<ctype.h>
#include<arpa/inet.h>

#include"wrap.h"

#define SERV_PORT 8000

int main(int argc, char *argv[]) {
    char buf[BUFSIZ];
    int clit_fd;

    struct sockaddr_in serv_addr;
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);

    clit_fd = Socket(AF_INET, SOCK_STREAM, 0); //不用bind,隐式绑定

    Connect(clit_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    int n;
    while(1) {
        n = Read(STDIN_FILENO, buf, sizeof(buf));
        Write(clit_fd, buf, n); //先从终端输入然后发给服务器处理,再读回客户端
        n = Read(clit_fd, buf, sizeof(buf));
        Write(STDOUT_FILENO, buf, n); //打印到终端展示  
    }

    close(clit_fd);

    return 0;
}

出错处理封装函数

把常用函数与错误处理函数封装在一起,在代码中将出错处理与逻辑分离

编译时要把server.c/client.c和wrap.c联合编译

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

wrap.c:

#include "wrap.h"

void perr_exit(const char *s) {
    perror(s);
    exit(-1);
}

int Socket(int family, int type, int protocol) {
//函数名首字母改为大写,既与原函数区分开,又能转到manpage
    int n;
    if ((n = socket(family, type, protocol)) < 0)
        perr_exit("socket error");
  
    return n;
 }
//以socket函数举例,完整代码见~/gua/tcp_socket/wrap.c


不准投币喔 👆

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇