Linux之Socket编程

本文详细介绍了Linux下的Socket编程,包括Socket基础知识、地址结构、字节序转换、套接字类型、TCP客户/服务器模型、连接与监听函数的使用,以及解决TCP连接中的粘包问题。通过实例代码展示了如何创建简单的服务器和客户端程序,同时也讨论了地址复用和多进程服务器处理多个客户端连接。此外,还探讨了TCP的三次握手和四次挥手过程。
摘要由CSDN通过智能技术生成

1.什么是Socket?

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),socket就提供了这些操作对应的函数接口。

socket可以看成是用户进程与内核网络协议栈的编程接口。

socket不仅可以用于本机的进程间通信,还可以 用于网络上不同主机的进程间通信。

image

2.IPv4套接口地址结构

IPV4套妾口地址结构通常也弥为“网际套接字地址结构”,它以 “sockaddr_in”命名,定义在头文件<netinet/in.h>中

struct sockaddr_in{
    uint8_t sin_len;    //整个sockaddr_in结构体的长度
    sa_family_t sin_family;    //指定该地址家族,在这里必须设为AF_INET
    in_port_t sin_port;    //端口(2字节)
    struct in_addr sin_addr;    //IPv4的地址(4字节)
    char sin_zero[8];     //暂不使用,一般将其设置为0 (8字节)
}

IPv4套接字一般只需关心3个字段

struct sockaddr_in {
    sa familytsin_family;/* address family:AF_INET */
    in_portt sin_port;/* port in network byte order */
    struct in_addr sin_addr;/* internet address */ 
}; /* Internet address.*/ 

struct in_addr {
    uint32_t s_addr;/* address in hetwork byte order
}

3.通用地址结构

通用地址结构用来指定与套接字关联的地址

struct sockaddr{
    uint8_t sin_len;    //整个sockaddr结构体的关度
    sa_ family_t sin_family;    //指定该地址家族
    char sa_data[14];    //由sin_family决定它的形式
}

因为不同的协议地址结构形式可能不一样,通用的可以用于任何协议的接口

一般将IPv4的sockaddr_in强行转换为通用的地址结构sockaddr

4.网络字节序

字节序 

    大端字节序(Big Endian) :最高有效位(MSB:Most Significant Bit)存储于最低内存地址 处,最低有效位(LSB:Lowest Significant Bit;存储于最高内存地坨处。

    小端字节序(l.ittle ndian) :最高有效位(MSB:Most Significant Bit)存储于最高内存地址 处,最低有效位(LSB:Lowest Significant Bit>存储于最低内存地垃处。

主机字节序

    不同的主有不同的字节序,如:86为小端字节序,Motorola 6800为 大端字节序,ARM字节序是可配置的。

网络字节序

    网络字节序规定为大端字节序

5c4ecf69-2757-4aa6-9301-679ccc1db48f

为了将字节序统一,就出现了网络字节序,为大端字节序

编写一个程序测试大小端 

#include<stdio.h>
int main(void)
{
    unsigned int x=0x12345678;
    unsigned char *p=(unsigned char*)&x;
    printf("%0x %0x %0x %0x\n",p[0],p[1],p[2],p[3]);
    return 0;
}

Linux下运行结果:

a645325a-b6a9-42ee-94e1-aad0f38c895c

说明是小端模式

5.字节序的转换函数

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

说明:在上述的函数中

h代表 host

n代表 network

s代表 short

I代表 long

程序测试网络字节序

#include<stdio.h>
#include<arpa/inet.h>
int main()
{
    unsigned int x = 0x12345678;
    unsigned int y=htonl(x);
    unsigned char *p=(unsigned char*)&y;
    printf("%0x %0x %0x %0x\n",p[0],p[1],p[2],p[3]);
    return 0;
}

运行结果:

0a315c5b-ec69-4bf3-ac38-41ea5eb18b15

可见,网络字节序会将本地字节序转换为大端模式

6.地址转换函数

#include <netinet/in.h>
#include <arpa/inet.h>

int inet_ aton(const char *cp,struct in_addr *inp);//将点分十进制转换为网络字节序的结构
in_addr_t inet_addr(const char *cp);    //将点分十进制IP地址转换为32位整数
char *inet_ntoa(struct in_addr in);    //将网络字节序的结构转换为点分十进制

一般的地址:点分十进制形式,如:192.168.0.100

测试程序:

int main()
{
    unsigned long addr=inet_addr("192.168.0.100");
    cout<<ntohl(addr)<<endl;
    return 0;
}
//运行结果:3232235620
int main()
{
    unsigned long addr=inet_addr("192.168.0.100");
    struct in_addr ipaddr;
    ipaddr.s_addr=addr;
    cout<<inet_ntoa(ipaddr)<<endl;
    return 0;
}
//运行结果:192.168.0.100

7.套接字类型

常用的三种:

流式套接字(SOCK_STREAM)  (TCP协议)

提供面向连接的、可靠的数据传输服务,数据无 差错,无重复的发送,且按发送顺序接收。

数据报式套接字(SOCK_DGRAM) (UDP协议)

提供无连接服务。不提供无错保证,数据可能丢 失或重复,并且接收顺序混乱。

原始套接字(SOCK_RAW)

可以将应用层直接封装成IP层能认识的协议格式

8.TCP客户/服务器模型

418fd341-7f58-4ab7-bbe4-c12704a911b8

9.回射客户/服务器

6da5d374-af3d-4cc9-93a1-810c402994fd

10.socket的基本函数


socket函数

头文件

<sys/socket.h>

功能:创建一个套接字用于通信 

原型 

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

参数 

domain:指定通信协议族(protocol family)

       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)
       AF_IPX              IPX - Novell protocols
       AF_NETLINK          Kernel user interface device     netlink(7)
       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
       AF_AX25             Amateur radio AX.25 protocol
       AF_ATMPVC           Access to raw ATM PVCs
       AF_APPLETALK        Appletalk                        ddp(7)
       AF_PACKET           Low level packet interface       packet(7)

type:指定socket类型,流式套接字SOCK_STREAM,数据报套 接字SOCK DGRAM,原始套接字SOCK RAW

       SOCK_STREAM     Provides  sequenced,  reliable,  two-way,  connection-based
                       byte  streams.   An out-of-band data transmission mechanism
                       may be supported.

       SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages  of
                       a fixed maximum length).

       SOCK_SEQPACKET  Provides  a  sequenced,  reliable, two-way connection-based
                       data transmission  path  for  datagrams  of  fixed  maximum
                       length;  a  consumer  is  required to read an entire packet
                       with each input system call.

       SOCK_RAW        Provides raw network protocol access.

       SOCK_RDM        Provides a reliable datagram layer that does not  guarantee
                       ordering.

       SOCK_PACKET     Obsolete  and  should  not  be  used  in  new programs; see
                       packet(7).
       SOCK_NONBLOCK   Set  the  O_NONBLOCK  file status flag on the new open file
                       description.  Using this flag saves extra calls to fcntl(2)
                       to achieve the same result.

       SOCK_CLOEXEC    Set  the  close-on-exec  (FD_CLOEXEC)  flag on the new file
                       descriptor.  See the description of the O_CLOEXEC  flag  in
                       open(2) for reasons why this may be useful.

protocol:协议类型 

返回值:成功返回非负整数,它与文件描述符类似,我们把它称为套接口描述字,简称套接字。失败返回-1

bind函数

功能:绑定一个本地地址到套接字

原型

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

参数

sockfd:socket函数返回的套接字

addr:要绑定的地址

addrlen:地址长度

返回值:成功返回0,失败返回-1

listen函数

功能:将套接字用于监听进入的连接

原型

int listen (int sockfd,int backlog);

参数

sockfd: socket函数返回的套接字口

backlog: 规定内核为此套接字排队的最大连妾个数口,表示已完成队列和未完成队列的组合,未完成队列表示三次握手还没有成功的条目

返回值:成功返回0,失败返回-1,规定了并发连接的数目

一般来说,listen函数应该在调用socket和bind函数之后,调用函数accept之前调用。

对于给定的监听套接口,内核要维护两个队列:

1、已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程

2、已完成连接的队列

// 调用listen函数后,就成了被动套接字,否则是主动套接字

// 主动套接字:发送连接(connect)

// 被动套接字:接收连接(accept)

384ad278-1aac-4d74-81b2-a8a6078a0845

accept函数

功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。

原型

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

sockfd:服务器套接字

addr:将返回对等方的套接字地址,相当于把对方的信息填充到结构体中

addrlen:返回对等方的套接字地址长度

返回值:成功返回非负整数,失败返回-1

connect函数

功能:建立一个连接至addr所指定的套接字

原型

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

参数

sockfd:未连接套接字

addr:要连接的套接字地址

addrlen:第二个参数addr长度

返回值:成功返回0,失败返回-1

发送/接收函数

服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作,即实现了网咯中不同进程之间的通信.网络I/O操作有下面几组:

#include <unistd.h>

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

#include <sys/types.h>
#include <sys/socket.h>

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

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。

close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

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

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。


11.简单的服务器客户端程序

回射服务器代码

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<error.h>
#include<string.h>
#include<iostream>
using namespace std;

#define ERR_EXIT(m) \
        do\
        {\
            perror(m);\
            exit(EXIT_FAILURE);\
        } while (0);

int main(void)
{
    //socket
    int listenfd;
    //listenfd=socket(PF_INET,SOCK_STREAM,0);
    if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
    {
        ERR_EXIT("socket");
    }

    //填充地址结构
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_port=htons(5188);
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //htonl可以省略,因为INADDR_ANY是全0的
    //servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    //inet_aton("127.0.0.1",&servaddr.sin_addr);

    //地址复用
    int on=1;
    if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
    {
        ERR_EXIT("setsocketopt");
    }

    //bind 绑定listenfd和本地地址结构
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
    {
        ERR_EXIT("bind");
    }

    if(listen(listenfd,SOMAXCONN)<0)
    {
        ERR_EXIT("listen");
    }

    // 调用listen函数后,就成了被动套接字,否则是主动套接字
    // 主动套接字:发送连接(connect)
    // 被动套接字:接收连接(accept)

    //对方的地址
    struct sockaddr_in peeraddr;
    socklen_t peerlen=sizeof(peeraddr);
    int conn;   //已连接套接字(主动)
    if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
    {
        ERR_EXIT("accept");
    }
    //连接成功后打印客户端的ip和端口
    printf("client: ip=%s | port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));

    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));

        int ret=read(conn,recvbuf,sizeof(recvbuf));
        fputs(recvbuf,stdout);
        write(conn,recvbuf,ret);
    }

    //关闭套接口
    close(conn);
    close(listenfd);
    return 0;
}

回射客户端代码

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

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0);

int main()
{
    //socket
    int sock;
    if((sock=s
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值