目录
一、对accept的参数重新定义
接着上篇建立好socket通道之后,这一次来实现让服务端和客户端能够进行一些简单的信息交互。上篇连接通道的代码如下:
接下来开始对连接进服务器的客户端发送一些字符,并且显示打印出连接进来的客户端IP地址及端口号。
首先获取listen监听后accept选出建立连接队列当中完成三次握手的客户端,获取该客户端的IP和端口号accept剩下的两个参数不能为NULL,第二个参数要使用struct aockaddr_in(不使用struct sockaddr是因为sockaddr_in好用)的结构体从而获取到客户端的IP和端口号第三个参数为第二个参数的大小并且取地址,accept函数原型为:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept的返回值为一个整形的描述符,用来向客户端发送内容。写好accept之后判断accept是否正常返回,失败返回值是-1。当有客户连接时accept成功就可以通过答应输出显示客户端的IP。打印时注意把网络格式的IP转为字符串形式,使用地址转换API:
inet_ntoa(struct in_addr inaddr);
二、用read和write实现信息的发和收
定义一个字符数组的buf来存放客户端发的内容,用read读取数据,write发送数据,如果接收端有乱码可能是定义的数组空间过大,可以通过定义字符串,直接发送字符串,可以避免
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int s_fd;
int c_fd;
char readbuf[128];
char *msg = "get msg success";
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
int s_len;
int c_len;
s_fd = socket(AF_INET,SOCK_STREAM,0);
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8888);
inet_aton("192.168.0.197",&s_addr.sin_addr);
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
listen(s_fd,10);
s_len = sizeof(struct sockaddr_in);
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&s_len);//最后一个参数为结构体大小的地址
printf("%s\n",inet_ntoa(c_addr.sin_addr));
// ssize_t read(int fd, void *buf, size_t count);
read(c_fd,readbuf,sizeof(readbuf));//读取readbuf中客户端发送的数据
printf("%s",readbuf);//打印出读取到的数据
// ssize_t write(int fd, const void *buf, size_t count);
write(c_fd,msg,sizeof(msg));//发送msg给接收端
return 0;
}
到这一步就已经可以通过telnet在两台设备来进行收发数据。window下可以cmd来测试,但还不能实现自由收发
window下telnet连接成功发送一个字符后收到服务端数据,这里出现了一个问题,收到的数据不是完整的,也就是可能write可能出了问题,后来发现write计算msg大小时用了sizeof,应该改为strlen,sizeof是计算的分配的空间大小,strlen计算的是字符串从第一个字符到\0时有多少个字符数不包括\0,而msg是一个char*型是一个地址,32位下的地址数据的大小为4个字节,64位下为8个字节。
三、编写客户端部分的代码实现数据收发自由(模拟两个用户聊天)
客户端也是第一步先创建套接字,基本步骤和服务端一致,然后通过connect连接服务器,connect的返回值用于判断连接状态没有像accept一样是个描述符,之后通过套接字的描述符来向客户端发送(write)接收(read)内容。为了方便,我把IP地址和端口号用参数形式来运行程序服务端也做同样的修改,其中参数端口号是以字符串形式传入的所以要用atoi()函数把字符串转为整数,不加会出错。
int main(int argc,char **argv )
{
int c_fd;
char readbuf[128];
char *msg = "message from client";
struct sockaddr_in c_addr;
int nread;
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
c_fd = socket(AF_INET,SOCK_STREAM,0);//选择TCP协议
if(c_fd == -1){
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));//选择端口号
inet_aton(argv[1],&c_addr.sin_addr);//字符串地址转化为网络能识别的的格式
//2.connect
// int connect(int sockfd, const struct sockaddr *addr,
// socklen_t addrlen);
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr))){//连接服务端
perror("connect");
exit(-1);
}
//3.send
write(c_fd,msg,strlen(msg));//发送字符串
//4.read
nread = read(c_fd,readbuf,128);//读取服务端发送的数据保存到缓存
if(nread == -1){
perror("read");
}else{
printf("get message from server: %d,%s\n",nread, readbuf);//输出缓存中的数据
}
return 0;
}
运行结果:结果正常
四、客户端与服务端无限制聊天
服务端和客户端在上一步位置互相发送接收一次信息就退出,想让服务端和客户端间不停的收发,且服务端还能继续保持随时接收用户连接的状态,可以通过fork()调用子进程实现,原理是服务端做一个循环一直让程序等待接收用户连接,当有用户连接时就fork()让子进程不停的做读取客户端数据的动作,同时也要能够随时的发送数据,所以需要再fork()一个子进程来单独做发送这件事。对于发送部分的子进程来说读取数据部分就是父进程,对读取部分来说accept等待接收用户部分是父进程。服务端代码如下:
#include<stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include<linux/in.h>
#include <arpa/inet.h>
//#include <sys/socket.h>
#include <stdlib.h>
#include<string.h>
#include <unistd.h>
int main(int argc,char **argv)
{
int s_fd;
char readbuf[128] = {0};
char msg[128] = {0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
int nread;
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1){
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
//2.bind
// int bind(int sockfd, const struct sockaddr *addr,
// socklen_t addrlen);
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
//4.accept
// int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int clen = sizeof(struct sockaddr_in);
while(1){
int c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("accept");
}
printf("connect\n");
if(fork() == 0){
// printf("get connet: %s\n",inet_ntoa(c_addr.sin_addr));
// printf("inet_ntoa ip = %s\n",inet_ntoa(c_addr.sin_addr));
//5.read
if(fork() == 0){
while(1){
printf("input: \n");
memset(msg,0,sizeof(msg));
gets(msg);
write(c_fd,msg,strlen(msg));
sleep(1);
}
}
while(1){
memset(readbuf,0,sizeof(readbuf));
nread = read(c_fd,readbuf,128);
if(nread == -1){
perror("read");
}else{
printf("get message: %d,%s\n",nread, readbuf);
}
//6.write
}
// break;
}
}
return 0;
}
客户端:
#include<stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include<linux/in.h>
#include <arpa/inet.h>
//#include <sys/socket.h>
#include <stdlib.h>
#include<string.h>
int main(int argc,char **argv )
{
int c_fd;
char readbuf[128] = {0};
char msg[128] = {0};
struct sockaddr_in c_addr;
int nread;
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1){
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
//2.connect
// int connect(int sockfd, const struct sockaddr *addr,
// socklen_t addrlen);
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr))){
perror("connect");
exit(-1);
}
// while(1){
if(fork() == 0){
while(1){
printf("input:\n");
memset(msg,0,sizeof(msg));
gets(msg);
write(c_fd,msg,strlen(msg));
sleep(1);
}
}
while(1){
memset(readbuf,0,sizeof(readbuf));
sleep(2);
nread = read(c_fd,readbuf,128);
if(nread == -1){
perror("read");
}else{
printf("get message from server: %d,%s\n",nread, readbuf);
}
}
// }
//3.send
//4.read
return 0;
}
补充:因为发送和接收的过程不在限制次数所以使用read之前要注意,用来放读取的缓存readbuf要在每一次read之前把readbuf的缓存memset清除,防止上一次读取的数据冗余,造成下一次读取的内容还包括上一次的读取内容。
运行结果如下:
两边可以自由的进行收发聊天就像微信聊天一样,并且不会自动退出,自由聊天功能实现。