Linux下的socket编程

Posted 图南

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux下的socket编程相关的知识,希望对你有一定的参考价值。

套接字(socket)

1.什么是套接字

  套接字(socket)是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。Linux所提供的功能(如打印服务、连接数据库和提供Web页面)和网络工具(如用于远程登录的rlogin和用于文件传输的ftp)通常都是通过套接字来进行通信的。

2.套接字连接

  你可以把套接字连接想象为打电话进一个繁忙的办公大楼。一个电话打到一家公司,接线员接听电话并把它转到正确的部门(服务器进程),然后再从那里转到电话要找的人(服务器套接字)。每个进入的电话呼叫(客户)都被转到正确的终端节点,而中间介入的接线员则可以空出来处理后续的电话。在开始学习Linux系统中的套接字连接是如何建立之前,我们需要先理解套接字应用程序是如何通过套接字来维持一个连接的。

  首先,服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他进程共享。

  接下来,服务器进程会给套接字起个名字。本地套接字的名字是Linux文件系统中的文件名,一般放在/tmp或/usr/tmp目录中。对于网络套接字,它的名字是与客户连接的特定网络有关的服务标识符(端口号或访问点)。这个标识符允许Linux将进入的针对特定端口号的连接转到正确的服务器进程。例如,Web服务器一般在80端口上创建一个套接字,这是一个专用于此目的的标识符。Web浏览器知道对于用户想要访问的Web站点,应该使用端口80来建立HTTP连接。我们用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个命名套接字。系统调用listen的作用是,创建一个队列并将其用于存放来自客户的进入连接。服务器通过系统调用accept来接受客户的连接。

  服务器调用accept时,它会创建一个与原有的命名套接字不同的新套接字。这个新套接字只用于与这个特定的客户进行通信,而命名套接字则被保留下来继续处理来自其他客户的连接。如果服务器编写得当,它就可以充分利用多个连接带来的好处。Web服务器就会这么做以同时服务来自许多客户的页面请求。对一个简单的服务器来说,后续的客户将在监听队列中等待,直到服务器再次准备就绪。

  基于套接字系统的客户端更加简单。客户首先调用socket创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向的数据通信。

2.1创建一个套接字:socket()

socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。

  #include <sys/types.h>

  #include <sys/socket.h>

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

  创建的套接字是一条通信线路的一个端点。domain参数指定协议族,type参数指定这个套接字的通信类型,protocol参数指定使用的协议。

  domain可选参数AF_INET、AF_INET6。AF_INET表示IPv4的socket,AF_INET6就表示IPv6的socket。

  type参数指定了socket类型。可选参数SOCK_STREAM(流格式套接字/面向连接的套接字)和 SOCK_DGRAM(数据报套接字/无连接的套接字)

  protocol参数一般socket类型中总会被指定为0。当protocol为0时,会自动选择type类型对应的默认协议。一般也常用IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

  socket()在成功时返回一个引用在后续系统调用中会用到的新创建的socket 的文件描述符

2.2套接字地址结构:struct sockaddr/ struct sockaddr_in/ struct sockaddr_un

  struct sockaddr:是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,这个类型的唯一用途是将各种domain特定的地址结构转换成单个类型以供socket系统调用中的各个参数使用。

  sockaddr_in:主要门用来保存 IPv4 地址的用于不同主机之间的socket编程。

  sockaddr_un:主要用于同一个主机中的本地socket用以进程间通信的一种方式。

 struct sockaddr  
      sa_family_t    sin_family;        //地址族
     char            sa_data[14];    //14字节,包含套接字中的目标地址和端口信息               
    ; 
 
 struct sockaddr_in
     sa_family_t        sin_family;       //地址族(Address Family),也就是地址类型
     uint16_t        sin_port;         //16位的端口号
     struct in_addr  sin_addr;        //32位IP地址
     char            sin_zero[8];      //不使用,一般用0填充
 ;
 struct sockaddr_un 
 
 
   sa_family_t    sun_family;             //AF_UNIX
   char            sun_path[UNIX_PATH_MAX]; //路径名
   ;

 

2.3将socket绑定到地址:bind()

  #include <sys/socket.h>

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

  sockfd参数是在上一个socket()调用中获得的文件描述符。addr参数是一个指针,它指向了一个指定该socket绑定到的地址的结构。addrlen参数指定了地址结构的大小。  

  一般来讲,会将一个服务器的 socket绑定到一个众所周知的地址——即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址,即初始化addr:

  addr.sin_family = AF_INET; //使用IPv4地址

  addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址

  addr.sin_port = htons(1234); //端口

  bind调用成功时返回0,失败时返回-1并设置errno为相应的值。

2.4创建套接字队列:listen()

  #include <sys/socket.h>

  int listen(int sockfd, int backlog);

  为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求。它用listen系统调用来完成这一工作。

  sockfd:为需要进入监听状态的套接字。

  backlog:为请求队列的最大长度。Linux系统可能会对队列中可以容纳的未处理连接的最大数目做出限制。为了遵守这个最大值限制,listen函数将队列长度设置为backlog参数的值。在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字。再往后的连接将被拒绝,导致客户的连接请求失败。listen函数提供的这种机制允许当服务器程序正忙于处理前一个客户请求的时候,将后续的客户连接放入队列等待处理。在 Linux 上,这个常量的值被定义成了128(可以修改)。

  listen调用成功时返回0,失败时返回-1并设置errno为相应的值。

2.5接受连接:accept()

  #include <sys/socket.h>

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

  一旦服务器程序创建并绑定了套接字之后,它就可以通过accept系统调用来等待客户建立对该套接字的连接。accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,后面和客户端通信时,使用新生成的套接字,而不是原来服务器端的套接字。监听socket (sockfd)会保持打开状态,并且可以被用来接受后续的连接。

  如果套接字队列中没有未处理的连接,accept将阻塞(程序将暂停)直到有客户建立连接为止。

2.6连接到对等的socket:connect()

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

  addr和 addrlen参数的指定方式与bind()调用中对应参数的指定方式相同。

  如果连接不能立刻建立,connect调用将阻塞一段不确定的超时时间。一旦这个超时时间到达连接将被放弃,connect调用失败。

2.7关闭套接字:close()

  int close(int sockfd);

  sockfd:要关闭的套接字描述符

  终止一个流 socket连接的常见方式是调用close()。如果多个文件描述符引用了同一个socket,那么当所有描述符被关闭之后连接就会终止。注意与的shutdown()区别

总结

  一个典型的流 socket 服务器会使用socket()创建其 socket,然后使用bind()将这个 socket绑定到一个众所周知(即需要通信的ip地址+端口)的地址上。服务器接着调用listen()以允许在该socket上接受连接。监听socket 上的客户端连接是通过accept()来接受的,它将返回一个与客户端的socket进行连接的新socket的文件描述符。

  一个典型的流socket客户端会使用socket()创建一个socket,然后通过调用connect()建立一个连接并制定服务器的众所周知的地址。当两个流socket连接之后就可以使用read()和 write()在任意一个方向上传输数据了。一旦拥有引用一个流 socket端点的文件描述符的所有进程都执行了一个隐式或显示的close()之后,连接就会终止。

参考资料

Linux程序设计+中文第4版

Linux_UNIX系统编程手册

C/C++ Linux Socket网络编程

之前已经学习了QT的socket编程 和 C/C++在window环境的socket编程,现在再来学习一波C/C++在Linux环境下的socket编程,为以后学习C++ Linux 服务器开发做准备。


目录

一、Socket简介

二、Socket编程基础

 1. 网络字节序

2. sockaddr数据结构

3. IP地址转换函数

三、Socket编程函数

1. socket函数

2. bind 函数

3. listen 函数

4. accept 函数

5. connect 函数

6. read 函数

7. write 函数

8. close 函数

四、回声服务器案例

1. 服务器

2. 客户端

3. 运行测试

五、总结


一、Socket简介

既然是socket,那必然有TCP 和 UDP之分,本文所记录的是TCP协议的socket编程。

socket编程分为TCP和UDP两个模块,其中TCP是可靠的、安全的,常用于发送文件等,而UDP是不可靠的、不安全的,常用作视频通话等。

如下图

 Socket通信3要素:

  1. 通信的目的地址;
  2. 使用的端口号;
  3. 使用的传输层协议(如TCP、UDP)

Socket通信模型

 Socket被称之为套接字。

在Linux环境中,Socket编程都是以伪文件的形式运行着;既然是文件,我们可以使用文件描述符引用套接字。(Linux一切皆文件

Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是文件主要应用于本地持久化数据的读写,而套接字多应用于网络进程间数据的传递。

在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。

套接字通信原理如下图所示:

在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符来发送缓冲区和接收缓冲区。

Socket 通信创建流程图


二、Socket编程基础

 1. 网络字节序

在计算机世界里,有两种字节序:

        大端字节序 --- 低地址高字节,高地址低字节

        小端字节序 --- 低地址低字节,高地址高字节

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。

网络数据流的地址有这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。

所以,我们在代码中必须要将ip地址和端口号做相应的转换,转换为网络字节序才可以进行通讯。

大多数使用 htonl 和 htons 。

为什么需要转换呢?

假设本地主机使用的是小端字节序,而对方主机使用的是大端字节序;你发送数据过去的地址顺序是:0x06b3,而对方接受到的却是:0xb306;这样数据就乱了,所以需要进行转换!

需要通过以下接口进行转换:

#include <arpa/inet.h>

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,l表示32位长整数,s表示16位短整数。

l 结尾的函数用于ip地址转换,s 结尾的函数用于端口号的转换。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。


2. sockaddr数据结构

我们在使用socket中,需要使用结构体sockaddr_in将IP地址和端口号等保存,然后用于绑定socket;

但进行绑定时,我们却要将结构体sockaddr_in强制类型转换为结构体sockaddr,这是为什么呢?

由于历史原因,一开始是没有结构体sockaddr_in,只有结构体sockaddr。

后来为了适配ipv4的到来,将结构体sockaddr细化为结构体sockaddr_in,如上图。

两个结构体如下:

struct sockaddr 
	sa_family_t sa_family; 		/* address family, AF_xxx */
	char sa_data[14];			/* 14 bytes of protocol address */
;

struct sockaddr_in 
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      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 network byte order */
;

IPv4的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些像bind 、accept函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,

例如:

struct sockaddr_in servaddr;

bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));        /* initialize servaddr */


3. IP地址转换函数

上面网络字节序中我们使用了htonl 和 ntohl 两个函数进行ip地址的转换,但只能将uint32_t类型的地址进行转换,例如:INADDR_ANY ==> 0.0.0.0

但是实际项目中我们设置ip地址大多数都是字符串,所以得使用特定的函数去进行转换。

#include <arpa/inet.h>

int inet_pton (int af, const char *src, void *dst);        // “本地ip转换为网络ip”

const char *inet_ntop (int af, const void *src, char *dst, socklen_t size);        // “网络ip转换为本地ip”

af 取值可选为 AF_INET 和 AF_INET6 ,即和 ipv4 和ipv6对应支持IPv4和IPv6;

src 是转换前ip,dst 是转换后ip;

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr。

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) 
	
	char ip[] = "6.7.8.9";
	char server_ip[64];
	
	struct sockaddr_in server_addr;
	
	inet_pton(AF_INET, ip, &server_addr.sin_addr.s_addr);
	
	printf("s_addr : %x\\n", server_addr.sin_addr.s_addr);
	
	printf("s_addr from net to host : %x \\n", ntohl(server_addr.sin_addr.s_addr));
	
	inet_ntop(AF_INET, &server_addr.sin_addr.s_addr, server_ip, sizeof(server_ip));
	
	printf("server_ip : %s \\n", server_ip);
	
	printf("INADDR_ANY: %d \\n", INADDR_ANY);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	inet_ntop(AF_INET, &server_addr.sin_addr.s_addr, server_ip, sizeof(server_ip));
	printf("INADDR_ANY ip : %s\\n", server_ip);
	
	return 0;

 运行结果:

s_addr : 9080706
s_addr from net to host : 6070809
server_ip : 6.7.8.9
INADDR_ANY: 0
INADDR_ANY ip : 0.0.0.0

ip地址:6.7.8.9

因为网络上使用的是大端字节序,所以通过inet_pton函数转换后的ip地址输出为:9080706

当通过ntohl函数转换回主机ip地址后输出为:6070809

因为我的本地主机使用的是小段字节序,所以转换后的循序和ip地址顺序一致,大端字节序则反过来了;

如果需要将网络的ip地址转换为字符串,则需要使用inet_ntop函数;

如果需要将字符串ip地址转换为网络ip地址,则需要使用inet_pton函数。

在服务器中,如果有多个网络,一般我们都会绑定所有网卡,会进行如下设置:

server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	// 监听本地所有IP地址

INADDR_ANY是一个宏,即为0的宏,他转换后赋值给结构体实际上是:0.0.0.0这个ip地址。


三、Socket编程函数

1. socket函数

#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

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

domain:

    AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址。

    AF_INET6 与上面类似,不过是来用IPv6的地址。

    AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用。

type:

    SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。

    SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。

    SOCK_SEQPACKET 该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。

    SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)。

    SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。

protocol:

    传 0 表示使用默认协议。

返回值:

    成功:返回指向新创建的socket的文件描述符,失败:返回 -1设置errno

可以使用以下方式进行打印输出失败报错信息:

fprintf(stderr, " errno:%s\\n", strerror(errno));

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

对于IPv4,domain 参数指定为AF_INET。

对于TCP协议,type 参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。

protocol 参数的介绍 - 略,指定为0即可。

例:

int sock;
sock = socket(AF_INET, SOCK_STREAM, 0);

2. bind 函数

#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

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

sockfd:

    socket文件描述符

addr:

    构造出IP地址加端口号的结构体

addrlen:

    sizeof(addr)长度

返回值:

    成功 返回 0失败 返回 -1, 设置 errno

 可以使用以下方式进行打印输出失败报错信息:

fprintf(stderr, " errno:%s\\n", strerror(errno));

    服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。

bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。

例:

struct sockaddr_in servaddr;            // 定义结构体
bzero(&servaddr, sizeof(servaddr));     // 将整个结构体清零

// 设置地址类型为AF_INET(IPv4)
servaddr.sin_family = AF_INET;          

/* 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,
   每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,
   直到与某个客户端建立了连接时才确定下来到底用哪个IP地址. */
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

// 设置端口号为5000
servaddr.sin_port = htons(5000);

// 绑定
bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr));

3. listen 函数

#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

int listen (int sockfd, int backlog);

sockfd:

    socket 文件描述符

backlog:

    在Linux 系统中,它是指排队等待建立3次握手队列长度。(客户端同时进行连接服务器的个数)

返回值:

    成功 返回 0失败 返回 -1, 设置 errno

 可以使用以下方式进行打印输出失败报错信息:

fprintf(stderr, " errno:%s\\n", strerror(errno));

查看一下系统默认backlog

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

改变 系统限制的backlog 大小

1. 打开文件

        vim /etc/sysctl.conf


2. 在文件最后添加
        net.core.somaxconn = 1024

        net.ipv4.tcp_max_syn_backlog = 1024


3. 保存,然后执行
        sysctl -p

如下图:(修改系统默认backlog为1024)

 

 为什么要修改呢?如果不修改,即使我们在代码里设置10240(listen(sock, 10240);),它也还是按照系统默认的值来设置的!

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。

例:

// 监听,同时监听128个请求
listen(sock, 128);

4. accept 函数

#include <sys/types.h>      /* See NOTES */

#include <sys/socket.h>

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

sockdf:

    socket文件描述符

addr:

    传出参数,返回连接客户端地址信息,含IP地址和端口号

addrlen:

    传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小

返回值:

    成功返回一个新的socket文件描述符,用于和客户端通信,失败返回 -1,设置errno

 可以使用以下方式进行打印输出失败报错信息:

fprintf(stderr, " errno:%s\\n", strerror(errno));

三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;如果给addr参数传NULL,表示不关心客户端的地址。

addrlen 参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。

例:

struct sockaddr_in client;
int client_sock;
		
socklen_t client_addr_len;		
client_addr_len = sizeof(client);
// 接受
client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);

5. connect 函数

客户端使用!

#include <sys/types.h>                 /* See NOTES */

#include <sys/socket.h>

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

sockdf:

    socket文件描述符

addr:

    传入参数,指定服务器端地址信息,含IP地址和端口号

addrlen:

    传入参数,传入sizeof(addr)大小

返回值:

   

  成功 返回 0失败 返回 -1, 设置 errno

 可以使用以下方式进行打印输出失败报错信息:

fprintf(stderr, " errno:%s\\n", strerror(errno));

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。

例:

int sockfd = 0;
struct sockaddr_in serveraddr;

sockfd = socket(AF_INET, SOCK_STREAM, 0);

serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr);
serveraddr.sin_port = htons(5000);

// 连接服务器
connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

6. read 函数

#include <unistd.h>

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

fd:

    socket文件描述符;

buf:

    存储读取到的数据,一般传char *类型或字符数组;

count:

    指定最多读取的大小。

返回值:

    读取成功返回读取到的字节数,读取失败返回 -1,设置errno

 可以使用以下方式进行打印输出失败报错信息:

fprintf(stderr, " errno:%s\\n", strerror(errno));

从socket文件符中,读取count指定的大小以内的数据存储到buf中。

例:

int client_sock;
char buf[256];
// 对client_sock的赋值这里省略...

len = read(client_sock, buf, sizeof(buf)-1);

7. write 函数

#include <unistd.h>

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

fd:

    socket文件描述符;

buf:

    需要发送(写入)的数据;

count:

    指定最多发送(写入)的大小。

返回值:

    发送(写入)成功返回写入的字节数,发送(写入)失败返回 -1,设置errno

 可以使用以下方式进行打印输出失败报错信息:

fprintf(stderr, " errno:%s\\n", strerror(errno));

例:

int client_sock;
char buf[256];
// 对client_sock的赋值这里省略...

len = write(client_sock, buf, sizeof(buf)-1);

8. close 函数

#include <unistd.h>

int close (int fd);

fd:

    socket文件描述符;

返回值:

    成功返回 0,失败返回 -1,并适当设置errno。

 可以使用以下方式进行打印输出失败报错信息:

fprintf(stderr, " errno:%s\\n", strerror(errno));

close()关闭一个文件描述符。

例:

int client_sock;
// client_sock= socket(AF_INET, SOCK_STREAM, 0);

close(client_sock);


四、回声服务器案例

描述:

客户端连接服务器,给服务器发送“hello world!”,服务器接收到后,将信息打印输出后,原封不动的给客户端发送回去,客户端接收到到后,也就数据打印输出,程序结束。

1. 服务器

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

#define SERVER_PORT 5000


int main(void) 
	
	int ret = 0;
	int sock;	// 通信套接字
	struct sockaddr_in server_addr;
	
	// 1.创建通信套接字
	sock = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sock) 
		fprintf(stderr, "create socket error, reason: %s\\n", strerror(errno));
		exit(-1);
	
	
	// 2.清空标签,写上地址和端口号
	bzero(&server_addr, sizeof(server_addr));
	
	server_addr.sin_family = AF_INET;	// 选择协议组ipv4
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	// 监听本地所有IP地址
	server_addr.sin_port = htons(SERVER_PORT);			// 绑定端口号
	
	// 3.绑定
	ret = bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
	if (-1 == ret) 
		fprintf(stderr, "socket bind error, reason: %s\\n", strerror(errno));
		close(sock);
		exit(-2);
	
	
	// 4.监听,同时监听128个请求
	ret = listen(sock, 128);
	if (-1 == ret) 
		fprintf(stderr, "listen error, reason: %s\\n", strerror(errno));
		close(sock);
		exit(-2);
	
	
	printf("等待客户端的链接\\n");
	
	int done = 1;
	
	while (done) 
		
		struct sockaddr_in client;
		int client_sock;
		char client_ip[64];
		int len = 0;
		char buf[256];
		
		socklen_t client_addr_len;		
		client_addr_len = sizeof(client);
		// 5.接受
		client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);
		if (-1 == client_sock) 
			perror("accept error");
			close(sock);
			exit(-3);
		
		
		// 打印客户端IP地址和端口号
		printf("client ip: %s\\t port: %d\\n",
				inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)),
				ntohs(client.sin_port));
		
		
		// 6.读取客户端发送的数据
		len = read(client_sock, buf, sizeof(buf)-1);
		if (-1 == len) 
			perror("read error");
			close(sock);
			close(client_sock);
			exit(-4);
		
		
		buf[len] = '\\0';
		printf("recive[%d]: %s\\n", len, buf);
		
		// 7.给客户端发送数据
		len = write(client_sock, buf, len);
		if (-1 == len) 
			perror("write error");
			close(sock);
			close(client_sock);
			exit(-5);
		
		
		printf("write finished. len: %d\\n", len);
		// 8.关闭客户端套接字
		close(client_sock);
	
	
	// 9.关闭服务器套接字
	close(sock);
	
	return 0;

2. 客户端

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



#define SERVER_PORT		5000
#define SERVER_IP		"127.0.0.1"


int main(int argc, char *argv[]) 
	
	int ret = 0;
	int sockfd = 0;			// 通信套接字
	char *message = NULL;
	struct sockaddr_in serveraddr;
	int n = 0;
	char buff[64];
	
	if (2 != argc) 	
		fprintf(stderr, "Usage: ./echo_client message \\n");
		exit(1);
	
	
	// 获取第二个参数的字符串
	message = argv[1];
	printf("message: %s\\n", message);
	
		// 1.创建通信套接字
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd) 
		perror("create sockfd error");
		exit(-1);
	
	
	// 2.清空标签,写上地址和端口号
	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;							// IPv4
	inet_pton(AF_INET, SERVER_IP, &serveraddr.sin_addr);		// 服务器地址
	serveraddr.sin_port = htons(SERVER_PORT);					// 服务器端口号
	
	// 3.连接服务器
	ret = connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
	if (-1 == ret) 
		perror("connect error");
		close(sockfd);
		exit(-2);
	
	
	// 4.给服务器发送数据
	ret = write(sockfd, message, strlen(message));
	if (-1 == ret) 
		perror("write error");
		close(sockfd);
		exit(-3);
	
	
	// 5.接受服务器发送过来的数据
	n = read(sockfd, buff, sizeof(buff)-1);
	if (-1 == n) 
		perror("read error");
		close(sockfd);
		exit(-4);
	
	
	if (n > 0) 
		buff[n] = '\\0';
		printf("receive: %s\\n", buff);
	 else 
		perror("error!!!\\n");
	
	
	printf("client finished.\\n");
	// 6.关闭套接字
	close(sockfd);
	
	return 0;

3. 运行测试

1. 服务器

ygt@YGT:~/echo_server$ gcc echo_server.c -o echo_server
ygt@YGT:~/echo_server$ ./echo_server
等待客户端的链接
client ip: 127.0.0.1     port: 41168
recive[12]: hello world!
write finished. len: 12

这里打印客户端的IP地址是127.0.0.1,是因为我是在同一台Linux系统中进行测试的,所以打印的是这个本地地址。

2. 客户端

root@YGT:/home/ygt/echo_server# gcc echo_client.c -o echo_client
root@YGT:/home/ygt/echo_server# ./echo_client "hello world!"
message: hello world!
receive: hello world!
client finished.

再来测试一下,在Linux中运行服务器程序,然后再window环境使用cmd控制台敲命令telnet去连接服务器。

才cmd中,telnet 后面接 服务器的ip地址 和 端口号

 当按下回车键后,就连接上服务器了,服务器也接受到了客户端的IP地址和端口号,并将其打印出来;然后客户端将字符 ‘h’ 发送给了服务器,服务器接收到后将其打印出来,然后给客户端也发送字符 'h',但是我们在cmd上是没有接收功能的,所以就没有接收到服务器发送过来的消息;最后服务器发送完成后就close断开了和客户端的连接,cmd这边就提示“遗失对主机的连接”。


五、总结

Linux环境中的C/C++ socket 与Window环境中的C/C++ socket类似。

创建服务器时需要按照指定流程来创建,根据上面图Socket 通信创建流程图来创建即可。

注意调用系统函数失败时,可以打印失败原因帮助我们定位问题。

以上是关于Linux下的socket编程的主要内容,如果未能解决你的问题,请参考以下文章

Linux下的socket编程

socket编程模拟linux下的ssh代码实现

LINUX下的Socket网络编程TCP/IP

socket编程在windows和linux下的区别是啥?

arm开发板上的linux如何通过网口和windows的网口通信,linux中的socket和windows下的socket能通用吗?

windows下的socket网络编程(入门级)