一,socket简介
1.什么是socket
2.网络字节序
#include <stdio.h>
int main()
{
unsigned int num = 0x12345678;
unsigned char *p = (unsigned char *)#
if (*p == 0x78)
{
printf("当前是小端机\n");
}
else if (*p == 0x12)
{
printf("当前是大端机\n");
}
else
{
printf("无法判断\n");
}
return 0;
}
二,基于TCP/IP协议的socket通信
socket抽象层与体系结构关系示意图
1. 基于TCP/ip的相关通信api简介

1)创建套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//建⽴⼀个新的socket(即为建⽴⼀个通信端⼝)
int socket(int domain, int type, int protocol);
成功返回⾮负的套接字描述符,失败返回 -1
参数说明:
domain:即协议域,⼜称为协议族(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)
AF_ALG Interface to kernel crypto API
type:
SOCK_STREAM TCP
SOCK_DGRAM UDP
SOCK_SEQPACKET 为最⼤⻓度固定的数据报提供有序、可靠、基于双向连接的数据
传输路径
SOCK_RAW 原始套接字
SOCK_RDM 提供不保证排序的可靠数据报层。
protocol:
⽤于指定socket所使⽤的传输协议编号,通常默认设置为0即可
0选择type类型对应的默认协议;
IPPROTO_TCP:TCP传输协议;
IPPROTO_UDP:UDP传输协议;
2)绑定套接字和服务器地址
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//⽤来给参数sockfd的socket设置⼀个名称,该名称由addr参数指向的sockadr结构
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrle
n);
返回说明:成功返回 0 失败返回 -1
⽤途:主要⽤与在TCP中的连接
形参说明:
sockfd 套接字⽂件描述符
addr 服务器地址信息
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
但在编程中⼀般使⽤下边这种等价结构sockaddr,对于IPV4我们常⽤这个结构
注意:使⽤该结构需要包含:#include <netinet/in.h>头⽂件 ****
struct sockaddr_in {
sa_family_t sin_family; IPV4对应AF_INET
//htons()
u_int16_t sin_port; 端⼝号//sin_port存储端⼝
号(使⽤⽹络字节顺序)
struct in_addr sin_addr; IP地址 //inet_addr()将
字符串形象ip转⽹络字节序
};
/* Internet address. */
struct in_addr {
u_int32_t s_addr; IP地址
};
addrlen addr的⻓度 sizeof(struct sockaddr)
//如果使⽤IPV6地址,需要⽤这个结构来定义变量存放ipv6相关信息
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
3)监听模式
#include <sys/socket.h>
//⽤于等待参数sockfd的scoket连线
int listen(int sockfd, int backlog);
返回值说明:成功返回0,失败返回-1
sockfd 套接字⽂件描述符
backlog 监听队列⻓度(等待连接的客户端的个数)缺省值20,最⼤值为128
即为规定了内核应该为相应套接⼝排队的最⼤连接个数
4)等待客户端连接的到来
#include <sys/types.h>
#include <sys/socket.h>
//接收socket的连线
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrle
n);
返回值说明:成功返回连接的客户端的套接字⽂件描述符 失败返回 -1
参数说明:
sockfd 服务器套接字⽂件描述符
addr 客户端信息地址,做返回值⽤的,不获取可以直接输⼊NULL
addrlen addr的⻓度,注意是⼀个指针类型 ,传⼊指定地址的⻓度,不指定
则NULL
5)客户端建立socket连线
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/*⽤于将参数sockfd的socket连接到参数addr指定的⽹络地址*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrl
en);
addr:服务端的地址
addrlen:为scokarr的结构⻓度
返回值:返回0时,表明该连接已经成功建⽴,返回-1时,表明该连接发⽣了错误,即没有成
功建⽴连接。
注意:connect() 函数不阻塞,所以在使⽤之前要确保服务器已经进⼊等待连接状态。
6)读写函数
#include<unistd.h>
//将数据写⼊已打开的⽂件内,写⼊count个字节到参数fd所指的⽂件内。
ssize_t write(int fd,const void*buf,size_t count);
//从已打开的⽂件中读取数据
ssize_t read(int fd,void*buf,size_t count);
返回值:读取到的实际数据数,如果返回0表示已经到达⽂件末尾或⽆可读取的数据,当read()函数
返回值为0时,
表示对端已经关闭了 socket,这时候也要关闭这个socket,否则会导致socket泄露。
当read()或者write()函数返回值⼤于0时,表示实际从缓冲区读取或者写⼊的字节数⽬
当read()或者write()返回-1时,⼀般要判断errno
⼀般是读写操作超时了,还未返回。这个超时是指socket的SO_RCVTIMEO与SO_SNDTIMEO两个属
性。
所以在使⽤阻塞socket时,不要将超时时间设置的过⼩。不然返回了-1,
你也不知道是socket连接是真的断开了,还是正常的⽹络抖动。⼀般情况下,阻塞的socket返回
了-1,
都需要关闭重新连接。
Close()和shutdown()——结束数据传输
当所有的数据操作结束以后,你可以调⽤close()函数来释放该socket,从⽽
停⽌在该socket上的任何数据操作:close(sockfd);
2.并发服务器
示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define PORT 8888
#define BUF_SIZE 1024
// 线程函数:处理一个客户端
void *handle_client(void *arg)
{
int client_sock = *(int *)arg;
free(arg);
char buffer[BUF_SIZE];
int n;
while ((n = read(client_sock, buffer, BUF_SIZE - 1)) > 0)
{
buffer[n] = '\0';
printf("[客户端消息] %s\n", buffer); // 在服务器端显示客户端消息
}
printf("客户端断开连接。\n");
close(client_sock);
return NULL;
}
int main()
{
int server_sock, *client_sock;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// 创建socket
server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock < 0)
{
perror("socket");
exit(EXIT_FAILURE);
}
int opt = 1;
setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("bind");
close(server_sock);
exit(EXIT_FAILURE);
}
if (listen(server_sock, 5) < 0)
{
perror("listen");
close(server_sock);
exit(EXIT_FAILURE);
}
printf("服务器已启动,监听端口 %d...\n", PORT);
while (1)
{
client_sock = malloc(sizeof(int));
*client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_len);
if (*client_sock < 0)
{
perror("accept");
free(client_sock);
continue;
}
printf("新客户端连接:%s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
pthread_t tid;
pthread_create(&tid, NULL, handle_client, client_sock);
pthread_detach(tid);
}
close(server_sock);
return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_SIZE 1024
int main()
{
int sock;
struct sockaddr_in server_addr;
char buffer[BUF_SIZE];
// 创建socket
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror("socket");
exit(EXIT_FAILURE);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("connect");
close(sock);
exit(EXIT_FAILURE);
}
printf("已连接到服务器,现在可以输入消息...\n");
while (1)
{
// 从标准输入读取消息
printf("输入: ");
if (fgets(buffer, BUF_SIZE, stdin) == NULL)
break;
// 发送到服务器
write(sock, buffer, strlen(buffer));
}
close(sock);
return 0;
}
运行结果:
三,基于UDP/IP协议的socket通信
基于UDP/IP通信的相关api简介
1)发送UDP报格式数据
#include <sys/types.h>
#include <sys/socket.h>
//把UDP数据报发给指定地址
int sendto (int sockfd, const void *buf, int len, unsigned int flags,
const struct sockaddr *to, int tolen);
参数说明:
sockfd 套接字⽂件描述符
buf 存放发送的数据
len 期望发送的数据⻓度
flags 0
to struct sockaddr_in类型,指明UDP数据发往哪⾥报
tolen: 对⽅地址⻓度,⼀般为:sizeof(struct sockaddr_in)。
2)接收UDP报格式数据
#include <sys/types.h>
#include <sys/socket.h>
//接收UDP的数据
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen);
参数意义和sentdo类似,其中romlen传递是接收到地址的⻓度
例如 int p=sizeof(struct adrr_in),最后⼀个参数就传为&p
3) 创建套接字 (Socket)
在进行任何网络通信之前,应用程序必须先向操作系统请求一个网络通信的“端点”,这个端点就是套接字。
#include <sys/types.h>
#include <sys/socket.h>
// 创建一个套接字
int socket(int domain, int type, int protocol);```
**参数说明:**
* `domain`: 指定通信协议族。对于IPv4网络通信,这个值通常是 `AF_INET`。
* `type`: 指定套接字的类型。
* 对于UDP通信,这个值必须是 `SOCK_DGRAM` (Datagram),表示这是一个提供数据报服务的套接字,不建立连接,每个数据包都是独立的。
* (作为对比,TCP使用 `SOCK_STREAM`)
* `protocol`: 协议类型。通常设置为 `0`,让系统根据 `domain` 和 `type` 自动选择最合适的协议(对于 `AF_INET` 和 `SOCK_DGRAM`,系统会选择IPPROTO_UDP)。
**返回值:**
* **成功**:返回一个新的文件描述符 (一个非负整数),代表创建的套接字。后续的所有操作都将通过这个文件描述符进行。
* **失败**:返回 `-1`,并设置全局变量 `errno` 来表示错误原因。
**示例:**
```c
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
4) 绑定地址和端口 (Bind)
对于需要接收数据的UDP程序(通常是服务器端),必须将其创建的套接字与一个具体的IP地址和端口号关联起来。这样,当网络中有数据包发送到这个IP和端口时,操作系统才知道应该将数据交给哪个程序处理。
#include <sys/types.h>
#include <sys/socket.h>
// 将套接字与一个地址绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd: socket() 函数返回的套接字文件描述符。
addr: 一个指向 struct sockaddr 结构体的指针,但实际使用中通常传入一个 struct sockaddr_in 结构体(需要进行类型强制转换)。该结构体包含了要绑定的IP地址和端口号。
addrlen: addr 结构体的长度,通常为 sizeof(struct sockaddr_in)。
struct sockaddr_in 结构体详解:
struct sockaddr_in {
sa_family_t sin_family; // 地址族, 必须是 AF_INET
in_port_t sin_port; // 端口号 (需要使用 htons() 转换)
struct in_addr sin_addr; // IPv4 地址
};
struct in_addr {
uint32_t s_addr; // 32位的IPv4地址 (需要使用 inet_addr() 或 INADDR_ANY)
};
注意: 端口号和IP地址必须从“主机字节序”转换为“网络字节序”。通常使用 htons() (Host to Network Short) 来转换端口,使用 inet_addr("127.0.0.1") 或 INADDR_ANY (表示绑定到本机所有IP地址) 来设置地址。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno。
示例:
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY; // 监听任意网卡的请求
servaddr.sin_port = htons(8888); // 绑定到8888端口
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
对于UDP客户端,通常不需要显式调用 bind()。因为在第一次调用 sendto() 时,操作系统会自动为其分配一个临时的端口号。
5) 关闭套接字 (Close)
当通信结束时,应该释放套接字占用的系统资源。这个操作就像关闭一个文件一样。
#include <unistd.h>
// 关闭一个文件描述符
int close(int fd);
参数说明:
fd: 需要关闭的文件描述符,这里就是 socket() 返回的套接字文件描述符。
返回值:
成功:返回 0。
失败:返回 -1,并设置 errno。
示例:
printf("通信结束,关闭套接字。\n");
close(sockfd);
总结:一个典型UDP程序的API调用流程
服务器端:
socket(): 创建套接字。
bind(): 为套接字绑定一个固定的IP和端口,以便客户端能找到它。
recvfrom(): 循环等待并接收来自客户端的数据。
sendto(): (可选)向客户端发送响应。
close(): 程序结束时关闭套接字。
客户端:
socket(): 创建套接字。
sendto(): 向服务器的指定IP和端口发送数据。
recvfrom(): (可选)接收来自服务器的响应。
close(): 程序结束时关闭套接字。