socket编程之实现一个简单的TCP通信

一、理解socket

1、socket即为套接字,在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一的标识网络通讯中的一个进程,“IP地址+TCP或UDP端口号”就为socket。
2、在TCP协议中,建立连接的两个进程(客户端和服务器)各自有一个socket来标识,则这两个socket组成的socket pair就唯一标识一个连接。
3、socket本身就有“插座”的意思,因此用来形容网络连接的一对一关系,为TCP/IP协议设计的应用层编程接口称为socket API。

二、网络字节序

内存中的多字节数据都有大小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大小端之分,同样,网络数据流也有大小端之分。
网络数据流的地址规定:先发出的数据时低地址,后发出的数据是高地址。发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,为了不使数据流乱序,接收主机也会把从网络上接收的数据按内存地址从低到高的顺序保存在接收缓冲区中。
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。
(PS:如果对大端字节序小端字节序不明白的童鞋们,可以看这篇文章参考一下:http://blog.csdn.net/qq_33951180/article/details/60767876

由于两端的两个主机的大小端不一定相同,因此为了使这些网络数据具有更强的可移植性,使相同的代码在大端和小端主机上都能正常运行,我们可以调用以下库函数进行网络字节序和主机字节序的相关转换:

#include<arpa/inet.h>

//将主机字节序转换为网络字节序
uint32_t htonl(uint32_t hostlong);//将32长整数从主机字节序转换为网络字节序,
                                  //如果主机字节序是小端,则函数会做相应大小
                                  //端转换后返回;如果主机字节序是大端,则函
                                  //数不做转换,将参数原封不动返回。。。下同
uint16_t htons(uint16_t hostshort);

//将网络字节序转换为主机字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

// h表示主机(host),n表示网络(net),l表示32位长整数,s表示16短整数。

三、TCP协议通讯的实现

TCP协议通讯流程:
这里写图片描述
我们先介绍几个函数:
1、创建套接字

int socket(int domain,int type,int protocol);
//domain:该参数一般被设置为AF_INET,表示使用的是IPv4地址。还有更多选项可以利用man查看该函数
//type:该参数也有很多选项,例如SOCK_STREAM表示面向流的传输协议,SOCK_DGRAM表示数据报,我们这里实现的是TCP,因此选用SOCK_STREAM,如果实现UDP可选SOCK_DGRAM
//protocol:协议类型,一般使用默认,设置为0

该函数用于打开一个网络通讯接口,出错则返回-1,成功返回一个socket(文件描述符),应用进程就可以像读写文件一样调用read/write在网络上收发数据。

2、绑定

int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
//sockfd:服务器打开的sock
//后两个参数可以参考第四部分的介绍

服务器所监听的网络地址和端口号一般是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind来绑定一个固定的网络地址和端口号。bind成功返回0,出错返回-1。
bind()的作用:将参数sockfd和addr绑定在一起,是sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。

3、监听

int listen(int sockfd,int backlog);
//sockfd的含义与bind中的相同。
//backlog参数解释为内核为次套接口排队的最大数量,这个大小一般为5~10,不宜太大(是为了防止SYN攻击)

该函数仅被服务器端使用,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

4、接收连接

int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
//addrlen是一个传入传出型参数,传入的是调用者的缓冲区cliaddr的长度,以避免缓冲区溢出问题;传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。

典型的服务器程序是可以同时服务多个客户端的,当有客户端发起连接时,服务器就调用accept()返回并接收这个连接,如果有大量客户端发起请求,服务器来不及处理,还没有accept的客户端就处于连接等待状态。
三次握手完成后,服务器调用accept()接收连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

5、请求连接

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

这个函数只需要有客户端程序来调用,调用该函数后表明连接服务器,这里的参数都是对方的地址。connect()成功返回0,出错返回-1。

了解这些函数后,我们来捋一捋客户端程序和服务器程序建立连接的过程:
服务器:首先调用socket()创建一个套接字用来通讯,其次调用bind()进行绑定这个文件描述符,并调用listen()用来监听端口是否有客户端请求来,如果有,就调用accept()进行连接,否则就继续阻塞式等待直到有客户端连接上来。连接建立后就可以进行通信了。
客户端:调用socket()分配一个用来通讯的端口,接着就调用connect()发出SYN请求并处于阻塞等待服务器应答状态,服务器应答一个SYN-ACK分段,客户端收到后从connect()返回,同时应答一个ACK分段,服务器收到后从accept()返回,连接建立成功。客户端一般不调用bind()来绑定一个端口号,并不是不允许bind(),服务器也不是必须要bind()。

思考题:为什么不建议客户端进行bind()?
答:当客户端没有自己进行bind时,系统随机分配给客户端一个端口号,并且在分配的时候,操作系统会做到不与现有的端口号发生冲突。但如果自己进行bind,客户端程序就很容易出现问题,假设在一个PC机上开启多个客户端进程,如果是用户自己绑定了端口号,必然会造成端口冲突,影响通信。


进行一番理论知识后我们就可以写代码了:
“server.c”

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int startup(int _port,const char* _ip)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        exit(1);
    }

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons( _port);
    local.sin_addr.s_addr = inet_addr(_ip);
    socklen_t len = sizeof(local);

    if(bind(sock,(struct sockaddr*)&local , len) < 0)
    {
        perror("bind");
        exit(2);
    }

    if(listen(sock, 5) < 0) //允许连接的最大数量为5
    {
        perror("listen");
        exit(3);
    }

    return sock;
}

int main(int argc,const char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:%s [loacl_ip] [loacl_port]\n",argv[0]);
        return 1;
    }

    int listen_sock = startup(atoi(argv[2]),argv[1]);//初始化

    //用来接收客户端的socket地址结构体
    struct sockaddr_in remote;
    socklen_t len = sizeof(struct sockaddr_in);

    while(1)
    {
        int sock = accept(listen_sock, (struct sockaddr*)&remote, &len);
        if(sock < 0)
        {
            perror("accept");
            continue;
        }
        printf("get a client, ip:%s, port:%d\n",inet_ntoa(remote.sin_addr),ntohs(remote.sin_port));
        char buf[1024];
        while(1)
        {
            ssize_t _s = read(sock, buf, sizeof(buf)-1);
            if(_s > 0)
            {
                buf[_s] = 0;
                printf("client:%s",buf);
            }
            else
            {
                printf("client is quit!\n");
                break;
            }
        }
    }
    return 0;
}

”client.c“

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main(int argc,const char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:%s [ip] [port]\n",argv[0]);
        return 0;
    }

    //创建一个用来通讯的socket
    int sock = socket(AF_INET,SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("socket");
        return 1;
    }

    //需要connect的是对端的地址,因此这里定义服务器端的地址结构体
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t len = sizeof(struct sockaddr_in);
    if(connect(sock, (struct sockaddr*)&server, len) < 0 )
    {
        perror("connect");
        return 2;
    }
    //连接成功进行收数据
    char buf[1024];
    while(1)
    {
        printf("send###");
        fflush(stdout);
        ssize_t _s = read(0, buf, sizeof(buf)-1);
        buf[_s] = 0;
        write(sock, buf, _s);
    }
    close(sock);
    return 0;
}

但是这样实现只能进行单进程通信,也就是说每次只能使一个客户端连接上进行数据通讯,这显然不符合服务器的基本要求。我们可以想办法修改服务器端的代码,每次accept成功之后就创建一个子进程,让子进程去处理读写数据,父进程继续监听并accept。
具体代码:https://github.com/lybb/Linux/tree/master/TCP_pro

修改的代码在服务器程序的socket()和bind()之间加入了如下的代码:

int opt=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
//设置sockfd的选项为SO_REUSEADDR为1,表示允许创建端口号相同但IP不同的多个
//socket描述符

但是如果是用创建子进程的方法比较浪费资源,我们可以修改为创建线程的方法

四、sockaddr数据结构

IPv4 和 IPv6 的地址格式定义在“netinet/in.h”中,IPv4用sockaddr_in结构体表示,包括16位端口号和32位IP地址;IPv6用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。

UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表⽰示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和UNIX Domain Socket的地 址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此socket API可以接受各种类型的sockaddr结构体指针做参数,例 如bind、accept、connect等函数,这些函数的参数应该设计成void 类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void 类型,因此这写函数的参数都用struct sockaddr*类型表示,在传参之前需要强制类型转换(在bind函数中就有用到)。

sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换。
字符串转in_addr的函数:
这里写图片描述

  • 61
    点赞
  • 501
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Socket编程是一种网络编程技术,它可以实现客户端与服务器之间的通信。在Socket编程中,客户端和服务器通过套接字(Socket)建立连接,然后通过Socket发送和接收数据。 客户端和服务器之间的通信可以分为两种方式:TCP和UDP。TCP是一种可靠的连接协议,它可以保证数据的可靠传输,但是速度较慢。UDP是一种不可靠的连接协议,它可以快速传输数据,但是数据的可靠性不如TCP。 在Socket编程中,客户端和服务器需要分别创建一个Socket对象,然后通过Socket对象建立连接。客户端通过Socket对象向服务器发送数据,服务器通过Socket对象接收数据,并且可以向客户端发送数据。 Socket编程需要掌握网络编程的基础知识,包括IP地址、端口号、协议等。同时,还需要了解Socket编程的相关API,如socket()、bind()、listen()、accept()、connect()、send()、recv()等函数。 Socket编程可以应用于各种网络应用程序,如Web服务器、邮件服务器、聊天程序等。它是网络编程中的重要技术之一,对于网络程序员来说,掌握Socket编程是必不可少的。 ### 回答2: Socket编程是一种在计算机网络中实现进程间通信的常用方式,可以用来实现客户端与服务器之间的通信实现Socket通信的核心在于建立连接,通过连接实现数据的发送和接收。下面,我们将从以下几个方面来说明Socket编程实现客户端与服务器通信的过程: 一、建立连接 客户端与服务器之间的连接可以采用TCP或UDP协议来建立。当采用TCP协议时,需要先建立三次握手,确保连接的可靠性,这会消耗一定的时间。而采用UDP协议则不需要建立连接,可以直接发送数据包,但是由于UDP协议不具备可靠性,可能会导致数据丢失或失序。 二、数据发送 在建立连接之后,客户端和服务器之间可以进行数据的发送和接收。客户端在发送数据时,需要先创建一个套接字,调用send()函数发送数据。服务器端在接收数据时,则需要先创建一个套接字,调用recv()函数接收数据。在数据发送时,可以选择发送数据的大小,也可以选择分包发送。 三、数据接收 当客户端发送数据之后,服务器端会接收到数据。服务器端需要先创建一个套接字,然后监听并接收客户端发送的数据。在接收数据时,服务器端也可以选择一次性接收全部数据,也可以选择分批次接收数据。 通过以上步骤,就可以实现客户端与服务器之间的通信。在实际应用中,还需要考虑多线程和多进程的应用,以提高系统的并发性能。同时,还需要加入安全措施,防止黑客攻击。总之,Socket编程是网络编程的基础,掌握Socket编程的方法和技巧,可以更好地实现客户端与服务器之间的通信。 ### 回答3: Socket编程是一种基于网络通信的编程技术,其主要目的是实现客户端与服务器之间的通信。在Socket编程中,客户端和服务器之间通过Socket通信协议建立连接,进行数据传输和通信Socket编程实现客户端与服务器通信的过程,通常包括以下几个步骤: 1. 创建Socket对象:客户端和服务器都需要创建Socket对象来进行通信。客户端使用Socket对象来连接服务器,服务器使用Socket对象来监听客户端请求并返回数据。 2. 连接服务器:客户端通过Socket对象连接服务器,可以指定服务器的IP地址和端口号。服务器通过监听客户端请求,接收客户端请求信息。 3. 发送消息:客户端创建输出流,向服务器发送消息。服务器通过读取客户端发送的消息,处理请求,并返回响应数据。 4. 接收消息:服务器创建输入流,读取客户端发送的数据并处理请求。服务器通过输出流向客户端返回响应数据。 5. 关闭Socket连接:当通信结束时,客户端和服务器均需关闭Socket连接Socket编程实现方式多种多样,可以使用不同的编程语言和工具进行开发。例如,Java语言可以使用Java Socket API实现Socket编程,C++可以使用Winsock或Socket类库实现Socket编程,Python可以使用socket模块实现Socket编程等。 在实际应用中,Socket编程广泛应用于各种网络应用场景,如Web服务器、FTP服务器、邮件服务器等。通过Socket编程,可以实现高效、可靠的网络通信,为各种应用提供了强有力的支持。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值