Socket通信详细解析(C++、TCP)

Socket通信实现详细步骤



说在前面

对套接字的理解:

  • 应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要 通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字 (Socket)的接口,区分不同应用程序进程间的网络通信和连接。(太难理解了。。。QAQ)
  • socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。
  • socket一词的起源
    在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”

推荐一篇前辈的文章,讲的特别好:
Socket网络编程

一、server端构建

  • 用结构体sockaddr_in创建一个本地通信地址,设置通信方式、端口、IP;
  • 创建Socket套接字
  • 调用bind函数将套接字和地址连接;
  • 调用listen函数,把内核设置为接收指向该套接口的连接请求;
  • 创建一个新的套接字,循环调用accept函数,返回值赋给该套接字
  • 调用recv函数获取消息的内容;
  • 通信完成后调用close函数关闭这个新创建的socket;
  • 如果不需要等待任何客户端连接,那么用close函数关闭server的socket;

1.创建本地通信地址

创建地址

#include<netinet/in.h>
struct sockaddr_in  servaddr;//服务器端地址

sockaddr_in是IPv4套接口地址结构(网际套接口地址结构);

struct sockaddr_in {
    uint8_t sin_len;      
    sa_family_t  sin_family;  
    in_port_t sin_port; 
    struct in_addr sin_addr;  
    char sin_zero[8];   
};
/*******************************/
 struct in_addr{  
	union {
         struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
         struct { u_short s_w1,s_w2; } S_un_w;
         u_long S_addr; 
    } S_un;
	#define s_addr  S_un.S_addr
};  

sockaddr_in详细解释
设置通信方式、IP、端口

memset(&servaddr, 0, sizeof(servaddr));//结构体清零
servaddr.sin_family = AF_INET;//设置使用ipv4进行通信
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//绑定到默认ip地址,将主机数转换成无符号长整型的网络字节顺序htonl:host to net long
servaddr.sin_port = htons(3100);//设置端口为3100

htonl()函数:把主机字节序转化为网络字节序(host to net long);
主机字节序和网络字节序的区别
htons()函数:把主机字节许转化为网络字节序(host to net short);
两种字节序之间相互转换的函数:

uint16_t    htons(uint16_t host16bitvalue);
uint32_t    htonl(uint32_t host32bitvalue);
uint16_t    ntohs(uint16_t net16bitvalue);
uint32_t    ntohl(uint32_t net32bitvalue);

2.创建套接字

listenfd = socket(AF_INET,SOCK_STREAM,0)

设定协议族为AF_INET(IPv4 协议),套接口类型为SOCK_STREAM(字节流套接口),协议设置为0,采用缺省配置;
socket函数原型:

#include<sys/socket.h>
int socket(int family, int type, int protocal);

参数:
family参数指明协议族

family              说明
AF_INET        IPv4 协议    
AF_INET6        IPv6 协议
AF_LOCAL        UNIX域协议
AF_ROUTE        路由套接口
AF_KEY            密钥套接口

type参数指明套接口类型

type              说明
SOCK_STREAM        字节流套接口
SOCK_DGRAM        数据报套接口
SOCK_SEQPACKET    有序分组套接口
SOCK_RAW        原始套接口

protocal参数指明某个协议类型常值,或者设置为0,以选择给定family和type组合的系统缺省值

protocal          说明
IPPROTO_TCP        TCP传输协议
IPPROTO_UDP        UDP传输协议
IPPROTO_SCTP        SCTP传输协议

这里注意,不是所有的family与type的组合都是有效的

socket函数成功时返回一个非负整数值,它与文件描述符类似.称其为套接口描述符(socket descriptor),简称套接字(sockfd).

3.调用bind()函数将服务器端地址和sever套接字连接

if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
}

bind函数原型:

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

sockfd参数是套接口标识;
myaddr参数是服务器端的地址;
addrlen参数是地址长度;

4.调用listen()函数打开套接口的监听模式

if( listen(listenfd, 10) == -1){
        printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
}

listen函数原型:

int listen(int sockfd, int backlog);

listen函数仅由TCP服务器调用.当socket函数创建一个套接口时,它被假设为一个主动套接口,即是一个将调用connect函数发起连接的客户端接口.而listen函数把一个未连接的套接口转换成一个 被动套接口,指示内核接收指向该套接口的连接请求.这将导致套接口状态从closed转换到listen.

5.循环调用accept()函数

调用accept()函数,将返回值赋给套接字connfd

if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){
      printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
      continue;
}

accept()函数原型:

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

参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址.
addrlen是一个值-结果参数:调用前我们将由*addrlen所引用的整数值置为由cliaddr所指的套接口地址结构的长度,返回时,该整数值即为由内核存在该套接口地址结构内的确切字节数.

6.调用recv()函数获取消息的内容

n = recv(connfd, buff, MAXLINE, 0);

函数原型:

int recv( SOCKET s, char *buf, int len, int flags);

s参数是socket套接字;
buf参数是指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
len参数指明缓冲区的长度;
flags参数一般置为0;

7.调用close()函数关闭创建的临时socket

close(connfd);

connfd参数为socket套接字;

8.不需要等待请求时,调用close()函数关闭server的套接字

close(listenfd);

完整代码:server.cpp

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

#define MAXLINE 16*1024

int main(int argc, char** argv){
    int  listenfd, connfd;
    struct sockaddr_in  servaddr;//服务器端地址
    char  buff[MAXLINE];
    int  n;
	//创建套接字
    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
        printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
    }

    memset(&servaddr, 0, sizeof(servaddr));//结构体清零
    servaddr.sin_family = AF_INET;//设置使用ipv4进行通信
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//绑定到默认ip地址,将主机数转换成无符号长整型的网络字节顺序htonl:host to net long
    servaddr.sin_port = htons(3100);//设置端口为3100
	//将服务器端地址和sever套接字连接
    if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
    }
	//设置套接字为监听状态
    if( listen(listenfd, 10) == -1){
        printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
    }

    printf("======waiting for client's request======\n");
    while(1){
        //接收的内容放到套接字connfd里
        if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){
            printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
            continue;
        }
        n = recv(connfd, buff, MAXLINE, 0);
        buff[n] = '\0';
        printf("recv msg from client: %s\n*****%d", buff,n);
        close(connfd);
    }
    close(listenfd);
    return 0;
}

二、client端构建

  • 创建client端的Socket套接字
  • 通过端口号和地址确定目标服务器
  • 用inet_pton将ip字符串转化为二进制
  • 使用Connect连接到服务器
  • 使用send发送消息
  • 通信完成后调用close关闭socket

1.创建client端的Socket套接字

跟server端的创建相似

if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
        return 0;
}

2.设置目标服务器地址

memset()函数的作用是将servaddr结构体里的变量都置为0;
端口号设置成与server一致,表明要连接的是我们刚才建立的那个server;

memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;//设置使用ipv4进行通信
servaddr.sin_port = htons(3100);//设置端口为3100

3.用inet_pton将ip字符串转化为二进制

if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
        printf("inet_pton error for %s\n",argv[1]);
        return 0;
}

将我们输入的目标IP参数(IPv4),转换为二进制存储到servaddr.sin_addr中.

此函数原型为:

 int inet_pton(int family, const char* strptr, void *addrptr);

inet_pton()函数将转换由指针strptr所指的串,并存到addrptr中(二进制结果),地址表达格式可以为IPv4(AF_INET),也可以是IPv6(AF_INET6);avg[1]是从控制台读入的第二个参数;

4.使用Connect连接到服务器

if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
        printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
}

connect函数原型:

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

这里sockfd是前面提到的由socket函数返回的套接口描述字,接着是一个指向套接口地址结构的指针和该结构的大小.套接口地址结构中必需包含有服务器的IP地址和端口号.端口号前面已经设定了为3100,IP地址在上一个步骤inet_pton函数中通过命令行参数argv[1]设定.
这个函数将client套接字和server的地址连接;

5.使用send发送消息

if( send(sockfd, sendline, strlen(sendline), 0) < 0){
        printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
}

函数原型:

int send( SOCKET s, const char FAR *buf, int len, int flags );

该函数的第一个参数指定发送端套接字描述符;
第二个参数指明一个存放应用程序要发送数据的缓冲区;
第三个参数指明实际要发送的数据的字节数;
第四个参数一般置0。
这里只描述同步Socket的send函数的执行流程。当调用该函数时,

(1)send先比较待发送数据的长度len和套接字s的发送缓冲的长度, 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;

(2)如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len

(3)如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完

(4)如果len小于剩余 空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。

如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执 行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)

注意:在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。详细解释

6.调用close关闭socket

close(sockfd);

整体代码:client.cpp

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#define MAXLINE 4096

int main(int argc, char** argv){
    int   sockfd, n;
    char  recvline[4096], sendline[4096];
    struct sockaddr_in  servaddr;
	//控制台传入要连接的端口ip
    if( argc != 2){
        printf("usage: ./client <ipaddress>\n");
        return 0;
    }
	//创建套接字
    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
        return 0;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;//设置使用ipv4进行通信
    servaddr.sin_port = htons(3100);//设置端口为3100

    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
        printf("inet_pton error for %s\n",argv[1]);
        return 0;
    }
	//将client套接字和端口连接
    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
        printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
        return 0;
    }

    printf("send msg to server: \n");
    fgets(sendline, 4096, stdin);
    if( send(sockfd, sendline, strlen(sendline), 0) < 0){
        printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    close(sockfd);
    return 0;
}

三、通信效果

将server.cpp和client.cpp分别编译成可执行文件
用makefile编译cpp文件的详细步骤见另一篇博客:
https://blog.csdn.net/qq_41748900/article/details/82316662
运行server:
在这里插入图片描述
运行client:(127.0.0.1是本地的ip)
在这里插入图片描述

参考文献:
https://www.cnblogs.com/zkfopen/p/9441264.html
https://blog.csdn.net/qq_27923041/article/details/83857964
https://blog.csdn.net/maopig/article/details/17193021#commentBox
https://zhidao.baidu.com/question/529573816.html
https://blog.csdn.net/kvew/article/details/1336577
http://blog.chinaunix.net/uid-14261758-id-2825546.html
https://blog.csdn.net/qq_39540224/article/details/79180349
https://www.cnblogs.com/tianlangshu/p/6795681.html

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值