目录
一、socket通信简介
二、socket通信的基本流程
三、socket服务器和客户端示例代码
1、服务端
2、客户端
3、运行结果
四、socket编程函数详解
1、socket()函数
2、bind()函数
3、网络字节序和主机字节序
4、listen()函数
5、accept()函数
6、connect()函数
7、close()、shutdown()函数
Socket通信是一种计算机网络中常见的通信机制,用于在不同计算机之间进行数据交换。Socket(套接字)是网络编程的基本组件之一,它提供了一种标准的编程接口,允许应用程序通过网络进行通信。
下面是Socket通信的一些基本概念和要点:
套接字(Socket): 套接字是通信的基本单元,它是网络通信的接口。套接字提供了一种封装了网络通信细节的抽象层,使得应用程序可以通过套接字进行数据的发送和接收。
通信协议: Socket通信可以使用不同的通信协议,其中最常见的是TCP(传输控制协议)和UDP(用户数据报协议)。TCP提供可靠的、面向连接的通信,而UDP是一种无连接的、不可靠的通信方式。
服务器端和客户端: 在Socket通信中,通常涉及两个主要角色:服务器端和客户端。服务器端等待并响应连接请求,而客户端发起连接请求并与服务器端建立连接。
地址和端口: 在Socket通信中,每个主机都有一个唯一的IP地址,而每个正在运行的应用程序则有一个端口号。通信双方通过IP地址和端口号来确定彼此的位置。
流程: Socket通信的基本流程包括创建套接字、绑定地址和端口、监听连接请求(对于服务器端)、接受连接请求(对于服务器端)、建立连接(对于客户端)、发送和接收数据、关闭连接等步骤。
阻塞和非阻塞: Socket通信可以是阻塞的或非阻塞的。在阻塞模式下,某些操作(如接受连接或发送数据)会导致程序阻塞,直到操作完成。非阻塞模式允许程序在等待操作完成时继续执行其他任务。
多线程和多进程: 在服务器端,可以使用多线程或多进程来处理多个连接,实现并发处理。每个连接都在独立的线程或进程中进行处理,以避免阻塞整个服务器。
Socket通信在网络编程中具有广泛的应用,例如Web服务器、聊天应用、文件传输等。它提供了一种灵活而强大的方式,使得不同计算机之间可以进行可靠的数据交换。
#include
#include
#include
#include
#include
#include
#include
#include
#define LISTEN_PORT 8889 //监听端口设为8889
#define BACKLOG 13 //相应socket可以在内核里排队的最大连接个数
int main(int argc , char *argv[])
{
int rv = -1;
int listen_fd = -1;
int client_fd = -1;
struct sockaddr_in ser_addr;
struct sockaddr_in cli_addr;
socklen_t cliaddr_len = sizeof(cli_addr);
char buf[1024];
listen_fd = socket(AF_INET,SOCK_STREAM,0);
if(listen_fd < 0)
{
printf("create socket failure : %s\n",strerror(errno));
return -1;
}
printf("socket create fd[%d]\n",listen_fd);
memset(&ser_addr,0,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(LISTEN_PORT);
ser_addr.sin_addr.s_addr= htonl(INADDR_ANY); //意味着监听所有的IP地址
if(bind(listen_fd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)) < 0)
{
printf("create socket failure : %s\n",strerror(errno));
return -2;
}
printf("socket[%d] bind on port[%d] for all IP address ok\n",listen_fd,LISTEN_PORT);
listen(listen_fd,BACKLOG);
while(1)
{
printf("\nStart waiting and accept new client connect.....\n",listen_fd);
client_fd = accept(listen_fd,(struct sockaddr*)&cli_addr,&cliaddr_len);
if(client_fd < 0)
{
printf("accept new socket failure : %s\n",strerror(errno));
return -3;
}
printf("Accept new client[%s:%d]with fd [%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),client_fd);
memset(buf,0,sizeof(buf));
if((rv = read(client_fd,buf,sizeof(buf))) < 0)
{
printf("Read data from client socket[%d] failure : %s\n",client_fd,strerror(errno));
close(client_fd);
continue;
}
else if(rv == 0)
{
printf("client socket[%d]disconnected\n",client_fd);
close(client_fd);
continue;
}
else if(rv > 0)
{
printf("read %d bytes data from client[%d] and echo it back :'%s'\n",rv,client_fd,buf);
}
if(write(client_fd,buf,rv) < 0)
{
printf("Write %d bytes data back to client[%d] failure : %s\n",rv,client_fd,strerror(errno));
close(client_fd);
}
sleep(1);
close(client_fd);
}
close(listen_fd);
}
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8889
#define MSG_STR "Hello,Unix Network Program World!"
int main(int argc,char *argv[])
{
int con_fd = -1;
int rv = -1;
struct sockaddr_in ser_addr;
char buf[1024];
con_fd = socket(AF_INET,SOCK_STREAM,0);
if(con_fd < 0)
{
printf("create socket failure : %s\n",strerror(errno));
return -1;
}
memset(&ser_addr,0,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SERVER_PORT);
inet_aton(SERVER_IP,&ser_addr.sin_addr);
if(connect(con_fd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)) < 0)
{
printf("connect to server [%s:%d] failure :%s\n",SERVER_IP,SERVER_PORT,strerror(errno));
return -2;
}
if(write(con_fd,MSG_STR,strlen(MSG_STR)) < 0)
{
printf("Write data to server failure : %s\n",strerror(errno));
goto cleanup;
}
memset(buf,0,sizeof(buf));
if((rv = read(con_fd,buf,sizeof(buf))) < 0)
{
printf("Read data from server failure :%s\n",strerror(errno));
goto cleanup;
}
else if(rv == 0)
{
printf("client connect to server failure get disconnected\n");
goto cleanup;
}
printf("Read %d bytes data from server:'%s'\n",rv,buf);
cleanup:
close(con_fd);
}
int socket(int domain, int type, int protocol);
socket() 函数是在Socket编程中用于创建套接字的函数,他会创建一个socket描述符,它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作有用到它,接下来叫我们来看看socket()函数的三个参数吧!
domain:即协议域,又称为协议族(family),常见的协议族有AF_INET、AF_INET6、AF_LOCAL,协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位)与端口号(16位)的组合。
type:指定socket类型。常用的有 SOCK_STREAM(流套接字,使用 TCP 协议)和 SOCK_DGRAM
(数据报套接字,使用 UDP 协议)。
protocol:指定协议,通常可设为0.当protocol为0时,会自动选择type类型对应的默认协议。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()
函数用于将一个套接字(socket)与一个特定的本地地址(通常是IP地址和端口号)关联起来。这个函数通常在服务器端创建套接字后,用于绑定套接字到一个具体的网络地址,以便监听来自该地址的连接请求。
接下来叫我们看看bind()函数的三个参数吧:
sockfd:即通过socket()
函数创建的套接字的文件描述符。
addlen:对应的是地址的长度。
addr:一个struct sockaddr
结构体的指针,包含要绑定的地址信息。这个地址结构根据地址创建socket时的协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针给内核。
ipv4对应的sockaddr_in类型定义(在头文件netinet/in.h中):
struct sockaddr_in {
unsigned short sin_family; // 地址族,通常设置为 AF_INET
uint16_t sin_port; // 端口号,网络字节序(Network Byte Order)
struct in_addr sin_addr; // IPv4地址结构,包含一个32位的IPv4地址
unsigned char sin_zero[8]; // 用于填充,保证结构体大小与 struct sockaddr 相同
};
其中,struct in_addr
是一个包含32位IPv4地址的结构体,定义如下:
struct in_addr {
in_addr_t s_addr; // 存储32位IPv4地址
};
网络字节序(Network Byte Order)和主机字节序(Host Byte Order)是两种不同的字节序(Byte Order)表示方式,它们在计算机网络通信中起到了重要的作用。
1. 网络字节序(Network Byte Order):
- 网络字节序是一种标准的字节序,用于在网络上传输数据。在网络字节序中,数据的高字节(Most Significant Byte,MSB)存储在内存的低地址处,而低字节(Least Significant Byte,LSB)存储在内存的高地址处。网络字节序是大端字节序(Big-Endian)的一种表示方式。
- 在网络通信中,为了保证不同计算机之间的数据交换的正确性,通常都使用网络字节序进行数据的表示和传输。
2. 主机字节序(Host Byte Order):
- 主机字节序是指在特定计算机体系结构中使用的字节序。不同计算机体系结构采用不同的字节序,其中有些是大端字节序,有些是小端字节序(Little-Endian)。
- 大部分个人计算机(如x86架构)使用小端字节序,即低字节存储在内存的低地址处。而一些其他体系结构(如PowerPC)使用大端字节序,即高字节存储在内存的低地址处。
在网络编程中,为了确保在不同计算机体系结构之间传输数据时不发生混淆,通常需要进行字节序的转换。函数 `htonl()`、`htons()`、`ntohl()`、`ntohs()` 是常用的字节序转换函数,它们分别表示将32位和16位的整数从主机字节序转换为网络字节序,以及从网络字节序转换为主机字节序。
- `htonl()`:Host to Network Long
- `htons()`:Host to Network Short
- `ntohl()`:Network to Host Long
- `ntohs()`:Network to Host Short
这些函数帮助确保在进行网络通信时,数据的字节序是符合网络字节序标准的,从而保证数据的正确传输。
listen()
函数用于使套接字处于监听状态,等待客户端的连接请求。具体来说,listen()
函数的作用是告诉操作系统,套接字已经准备好接受来自客户端的连接请求,并设置套接字的状态为监听状态。
int listen(int sockfd, int backlog);
sockfd:即通过socket()
函数创建的套接字的文件描述符。
backlog:相应的socket可以在内核里排队的最大连接个数。
accept()
函数用于接受客户端的连接请求,创建一个新的套接字来处理与客户端的通信。具体来说,accept()
函数会等待客户端发起连接请求,一旦有连接请求到达,它会创建一个新的套接字用于与该客户端进行通信,而原始的套接字仍然处于监听状态,可以继续接受其他连接请求。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:监听套接字的文件描述符,即通过 socket()
和 bind()
函数创建并绑定的套接字.
*addr :用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等。
addlen:返回客户端协议地址的长度。
accept()
函数的调用会阻塞程序的执行,直到有客户端连接请求到达。一旦有连接请求到达,accept()
的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()函数从该fd里读数据即可。一个服务器通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务区进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应该把客户端相应的socket描述字关闭。
accept()系统调用将会把客户端的信息保存在cli_addr这个结构体变量中,我们知道cli_addr是struct sockaddr_in这种IPV4地址类型,客户端的IP地址和端口号都保存在该结构体中。当然在该结构体中IP地址是以32位整形值的形式存放,端口号也是以网络字节序的形式存放的。这时我们可以使用inet_ntoa()函数将32位整形的IP地址转换成点分十进制字符串格式的IP地址“127.0.0.1”,我们也可以调用ntohs()函数将网络字节序的端口号转换成主机字节序的端口号。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect()函数用于与服务器建立连接。如果客户端这时调用connect发出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到客户的TCP连接,通过两个文件描述符(客户端connect的fd和服务端accept返回的fd)就可以实现客户端和服务端的相互通信。
IP地址“127.0.0.1”这是点分十进制形式的字符串形式,而在结构体struct sockaddr_in中IP
地址是以32位数据保存的,这时我们可以调用inet_aton()函数将点分十进制字符串转换成32位整形类型。同样,端口号也要使用htons()函数从主机字节序转换成网络字节序。
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字。close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用。
int close(int fd);
如果对socket fd调用close()则会触发该TCP连接断开的四次握手,有些时候我们需要数据发送出去之后并到达对方之后才能关闭socket套接字,则可以调用shutdown()函数来半关闭套接字:
int shutdown(int sockfd, int how);
如果how的值为SHUT_RD,则该套接字不可再读入数据了;如果how的值为SHUT_WR,则该套接字不可在发送数据了;如果how的值为SHUT_RDWR,则该套接字既不可读也不可写数据了。